*(minor): small refactor
This commit is contained in:
0
passbook/sources/ldap/__init__.py
Normal file
0
passbook/sources/ldap/__init__.py
Normal file
5
passbook/sources/ldap/admin.py
Normal file
5
passbook/sources/ldap/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Passbook LDAP Admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_sources_ldap')
|
||||
11
passbook/sources/ldap/apps.py
Normal file
11
passbook/sources/ldap/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Passbook ldap app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookSourceLDAPConfig(AppConfig):
|
||||
"""Passbook ldap app config"""
|
||||
|
||||
name = 'passbook.sources.ldap'
|
||||
label = 'passbook_sources_ldap'
|
||||
verbose_name = 'passbook Sources.LDAP'
|
||||
23
passbook/sources/ldap/auth.py
Normal file
23
passbook/sources/ldap/auth.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""passbook LDAP Authentication Backend"""
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.ldap.ldap_connector import LDAPConnector
|
||||
from passbook.sources.ldap.models import LDAPSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class LDAPBackend(ModelBackend):
|
||||
"""Authenticate users against LDAP Server"""
|
||||
|
||||
def authenticate(self, **kwargs):
|
||||
"""Try to authenticate a user via ldap"""
|
||||
if 'password' not in kwargs:
|
||||
return None
|
||||
for source in LDAPSource.objects.filter(enabled=True):
|
||||
_ldap = LDAPConnector(source)
|
||||
user = _ldap.auth_user(**kwargs)
|
||||
if user:
|
||||
return user
|
||||
return None
|
||||
50
passbook/sources/ldap/forms.py
Normal file
50
passbook/sources/ldap/forms.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""passbook LDAP Forms"""
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.core.forms.policies import GENERAL_FIELDS
|
||||
from passbook.sources.ldap.models import LDAPGroupMembershipPolicy, LDAPSource
|
||||
|
||||
|
||||
class LDAPSourceForm(forms.ModelForm):
|
||||
"""LDAPSource Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = LDAPSource
|
||||
fields = SOURCE_FORM_FIELDS + ['server_uri', 'bind_cn', 'bind_password',
|
||||
'type', 'domain', 'base_dn', 'create_user',
|
||||
'reset_password']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'server_uri': forms.TextInput(),
|
||||
'bind_cn': forms.TextInput(),
|
||||
'bind_password': forms.TextInput(),
|
||||
'domain': forms.TextInput(),
|
||||
'base_dn': forms.TextInput(),
|
||||
'policies': FilteredSelectMultiple(_('policies'), False)
|
||||
}
|
||||
labels = {
|
||||
'server_uri': _('Server URI'),
|
||||
'bind_cn': _('Bind CN'),
|
||||
'base_dn': _('Base DN'),
|
||||
}
|
||||
|
||||
|
||||
class LDAPGroupMembershipPolicyForm(forms.ModelForm):
|
||||
"""LDAPGroupMembershipPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = LDAPGroupMembershipPolicy
|
||||
fields = GENERAL_FIELDS + ['dn', ]
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'dn': forms.TextInput(),
|
||||
}
|
||||
labels = {
|
||||
'dn': _('DN')
|
||||
}
|
||||
293
passbook/sources/ldap/ldap_connector.py
Normal file
293
passbook/sources/ldap/ldap_connector.py
Normal file
@ -0,0 +1,293 @@
|
||||
"""Wrapper for ldap3 to easily manage user"""
|
||||
from time import time
|
||||
|
||||
import ldap3
|
||||
import ldap3.core.exceptions
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.sources.ldap.models import LDAPSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
USERNAME_FIELD = CONFIG.y('ldap.username_field', 'sAMAccountName')
|
||||
LOGIN_FIELD = CONFIG.y('ldap.login_field', 'userPrincipalName')
|
||||
|
||||
|
||||
class LDAPConnector:
|
||||
"""Wrapper for ldap3 to easily manage user authentication and creation"""
|
||||
|
||||
_server = None
|
||||
_connection = None
|
||||
_source = None
|
||||
|
||||
def __init__(self, source: LDAPSource):
|
||||
self._source = source
|
||||
|
||||
if not self._source.enabled:
|
||||
LOGGER.debug("LDAP not Enabled")
|
||||
|
||||
# if not con_args:
|
||||
# con_args = {}
|
||||
# if not server_args:
|
||||
# server_args = {}
|
||||
# Either use mock argument or test is in argv
|
||||
# if mock or any('test' in arg for arg in sys.argv):
|
||||
# self.mock = True
|
||||
# self.create_users_enabled = True
|
||||
# con_args['client_strategy'] = ldap3.MOCK_SYNC
|
||||
# server_args['get_info'] = ldap3.OFFLINE_AD_2012_R2
|
||||
# if self.mock:
|
||||
# json_path = os.path.join(os.path.dirname(__file__), 'tests', 'ldap_mock.json')
|
||||
# self._connection.strategy.entries_from_json(json_path)
|
||||
|
||||
self._server = ldap3.Server(source.server_uri) # Implement URI parsing
|
||||
self._connection = ldap3.Connection(self._server, raise_exceptions=True,
|
||||
user=source.bind_cn,
|
||||
password=source.bind_password)
|
||||
|
||||
self._connection.bind()
|
||||
# if CONFIG.y('ldap.server.use_tls'):
|
||||
# self._connection.start_tls()
|
||||
|
||||
# @staticmethod
|
||||
# def cleanup_mock():
|
||||
# """Cleanup mock files which are not this PID's"""
|
||||
# pid = os.getpid()
|
||||
# json_path = os.path.join(os.path.dirname(__file__), 'test', 'ldap_mock_%d.json' % pid)
|
||||
# os.unlink(json_path)
|
||||
# LOGGER.debug("Cleaned up LDAP Mock from PID %d", pid)
|
||||
|
||||
# def apply_db(self):
|
||||
# """Check if any unapplied LDAPModification's are left"""
|
||||
# to_apply = LDAPModification.objects.filter(_purgeable=False)
|
||||
# for obj in to_apply:
|
||||
# try:
|
||||
# if obj.action == LDAPModification.ACTION_ADD:
|
||||
# self._connection.add(obj.dn, obj.data)
|
||||
# elif obj.action == LDAPModification.ACTION_MODIFY:
|
||||
# self._connection.modify(obj.dn, obj.data)
|
||||
|
||||
# # Object has been successfully applied to LDAP
|
||||
# obj.delete()
|
||||
# except ldap3.core.exceptions.LDAPException as exc:
|
||||
# LOGGER.error(exc)
|
||||
# LOGGER.debug("Recovered %d Modifications from DB.", len(to_apply))
|
||||
|
||||
# @staticmethod
|
||||
# def handle_ldap_error(object_dn, action, data):
|
||||
# """Custom Handler for LDAP methods to write LDIF to DB"""
|
||||
# LDAPModification.objects.create(
|
||||
# dn=object_dn,
|
||||
# action=action,
|
||||
# data=data)
|
||||
|
||||
# @property
|
||||
# def enabled(self):
|
||||
# """Returns whether LDAP is enabled or not"""
|
||||
# return CONFIG.y('ldap.enabled')
|
||||
|
||||
@staticmethod
|
||||
def encode_pass(password):
|
||||
"""Encodes a plain-text password so it can be used by AD"""
|
||||
return '"{}"'.format(password).encode('utf-16-le')
|
||||
|
||||
def generate_filter(self, **fields):
|
||||
"""Generate LDAP filter from **fields."""
|
||||
filters = []
|
||||
for item, value in fields.items():
|
||||
filters.append("(%s=%s)" % (item, value))
|
||||
ldap_filter = "(&%s)" % "".join(filters)
|
||||
LOGGER.debug("Constructed filter: '%s'", ldap_filter)
|
||||
return ldap_filter
|
||||
|
||||
def lookup(self, ldap_filter: str):
|
||||
"""Search email in LDAP and return the DN.
|
||||
Returns False if nothing was found."""
|
||||
try:
|
||||
self._connection.search(self._source.search_base, ldap_filter)
|
||||
results = self._connection.response
|
||||
if len(results) >= 1:
|
||||
if 'dn' in results[0]:
|
||||
return str(results[0]['dn'])
|
||||
except ldap3.core.exceptions.LDAPNoSuchObjectResult as exc:
|
||||
LOGGER.warning(exc)
|
||||
return False
|
||||
except ldap3.core.exceptions.LDAPInvalidDnError as exc:
|
||||
LOGGER.warning(exc)
|
||||
return False
|
||||
return False
|
||||
|
||||
def _get_or_create_user(self, user_data):
|
||||
"""Returns a Django user for the given LDAP user data.
|
||||
If the user does not exist, then it will be created."""
|
||||
attributes = user_data.get("attributes")
|
||||
if attributes is None:
|
||||
LOGGER.warning("LDAP user attributes empty")
|
||||
return None
|
||||
# Create the user data.
|
||||
field_map = {
|
||||
'username': '%(' + USERNAME_FIELD + ')s',
|
||||
'name': '%(givenName)s %(sn)s',
|
||||
'email': '%(mail)s',
|
||||
}
|
||||
user_fields = {}
|
||||
for dj_field, ldap_field in field_map.items():
|
||||
user_fields[dj_field] = ldap_field % attributes
|
||||
|
||||
# Update or create the user.
|
||||
user, created = User.objects.update_or_create(
|
||||
defaults=user_fields,
|
||||
username=user_fields.pop('username', "")
|
||||
)
|
||||
|
||||
# Update groups
|
||||
# if 'memberOf' in attributes:
|
||||
# applicable_groups = LDAPGroupMapping.objects.f
|
||||
# ilter(ldap_dn__in=attributes['memberOf'])
|
||||
# for group in applicable_groups:
|
||||
# if group.group not in user.groups.all():
|
||||
# user.groups.add(group.group)
|
||||
# user.save()
|
||||
|
||||
# If the user was created, set them an unusable password.
|
||||
if created:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
# All done!
|
||||
LOGGER.debug("LDAP user lookup succeeded")
|
||||
return user
|
||||
|
||||
def auth_user(self, password, **filters):
|
||||
"""Try to bind as either user_dn or mail with password.
|
||||
Returns True on success, otherwise False"""
|
||||
filters.pop('request')
|
||||
if not self._source.enabled:
|
||||
return None
|
||||
# FIXME: Adapt user_uid
|
||||
# email = filters.pop(CONFIG.y('passport').get('ldap').get, '')
|
||||
email = filters.pop('email')
|
||||
user_dn = self.lookup(self.generate_filter(**{LOGIN_FIELD: email}))
|
||||
if not user_dn:
|
||||
return None
|
||||
# Try to bind as new user
|
||||
LOGGER.debug("Binding as '%s'", user_dn)
|
||||
try:
|
||||
temp_connection = ldap3.Connection(self._server, user=user_dn,
|
||||
password=password, raise_exceptions=True)
|
||||
temp_connection.bind()
|
||||
if self._connection.search(
|
||||
search_base=self._source.search_base,
|
||||
search_filter=self.generate_filter(**{LOGIN_FIELD: email}),
|
||||
search_scope=ldap3.SUBTREE,
|
||||
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
|
||||
get_operational_attributes=True,
|
||||
size_limit=1,
|
||||
):
|
||||
response = self._connection.response[0]
|
||||
# If user has no email set in AD, use UPN
|
||||
if 'mail' not in response.get('attributes'):
|
||||
response['attributes']['mail'] = response['attributes']['userPrincipalName']
|
||||
return self._get_or_create_user(response)
|
||||
LOGGER.warning("LDAP user lookup failed")
|
||||
return None
|
||||
except ldap3.core.exceptions.LDAPInvalidCredentialsResult as exception:
|
||||
LOGGER.debug("User '%s' failed to login (Wrong credentials)", user_dn)
|
||||
except ldap3.core.exceptions.LDAPException as exception:
|
||||
LOGGER.warning(exception)
|
||||
return None
|
||||
|
||||
def is_email_used(self, mail):
|
||||
"""Checks whether an email address is already registered in LDAP"""
|
||||
if self._source.create_user:
|
||||
return self.lookup(self.generate_filter(mail=mail))
|
||||
return False
|
||||
|
||||
def create_ldap_user(self, user, raw_password):
|
||||
"""Creates a new LDAP User from a django user and raw_password.
|
||||
Returns True on success, otherwise False"""
|
||||
if self._source.create_user:
|
||||
LOGGER.debug("User creation not enabled")
|
||||
return False
|
||||
# The dn of our new entry/object
|
||||
username = user.pk.hex # UUID without dashes
|
||||
# sAMAccountName is limited to 20 chars
|
||||
# https://msdn.microsoft.com/en-us/library/ms679635.aspx
|
||||
username_trunk = username[:20] if len(username) > 20 else username
|
||||
# AD doesn't like sAMAccountName's with . at the end
|
||||
username_trunk = username_trunk[:-1] if username_trunk[-1] == '.' else username_trunk
|
||||
user_dn = 'cn=' + username + ',' + self._source.search_base
|
||||
LOGGER.debug('New DN: %s', user_dn)
|
||||
attrs = {
|
||||
'distinguishedName': str(user_dn),
|
||||
'cn': str(username),
|
||||
'description': 't=' + str(time()),
|
||||
'sAMAccountName': str(username_trunk),
|
||||
'givenName': str(user.name),
|
||||
'displayName': str(user.username),
|
||||
'name': str(user.name),
|
||||
'mail': str(user.email),
|
||||
'userPrincipalName': str(username + '@' + self._source.domain),
|
||||
'objectClass': ['top', 'person', 'organizationalPerson', 'user'],
|
||||
}
|
||||
try:
|
||||
self._connection.add(user_dn, attributes=attrs)
|
||||
except ldap3.core.exceptions.LDAPException as exception:
|
||||
LOGGER.warning("Failed to create user ('%s'), saved to DB", exception)
|
||||
# LDAPConnector.handle_ldap_error(user_dn, LDAPModification.ACTION_ADD, attrs)
|
||||
LOGGER.debug("Signed up user %s", user.email)
|
||||
return self.change_password(raw_password, mail=user.email)
|
||||
|
||||
def _do_modify(self, diff, **fields):
|
||||
"""Do the LDAP modification itself"""
|
||||
user_dn = self.lookup(self.generate_filter(**fields))
|
||||
try:
|
||||
self._connection.modify(user_dn, diff)
|
||||
except ldap3.core.exceptions.LDAPException as exception:
|
||||
LOGGER.warning("Failed to modify %s ('%s'), saved to DB", user_dn, exception)
|
||||
# LDAPConnector.handle_ldap_error(user_dn, LDAPModification.ACTION_MODIFY, diff)
|
||||
LOGGER.debug("modified account '%s' [%s]", user_dn, ','.join(diff.keys()))
|
||||
return 'result' in self._connection.result and self._connection.result['result'] == 0
|
||||
|
||||
def disable_user(self, **fields):
|
||||
"""Disables LDAP user based on mail or user_dn.
|
||||
Returns True on success, otherwise False"""
|
||||
diff = {
|
||||
'userAccountControl': [(ldap3.MODIFY_REPLACE, [str(66050)])],
|
||||
}
|
||||
return self._do_modify(diff, **fields)
|
||||
|
||||
def enable_user(self, **fields):
|
||||
"""Enables LDAP user based on mail or user_dn.
|
||||
Returns True on success, otherwise False"""
|
||||
diff = {
|
||||
'userAccountControl': [(ldap3.MODIFY_REPLACE, [str(66048)])],
|
||||
}
|
||||
return self._do_modify(diff, **fields)
|
||||
|
||||
def change_password(self, new_password, **fields):
|
||||
"""Changes LDAP user's password based on mail or user_dn.
|
||||
Returns True on success, otherwise False"""
|
||||
diff = {
|
||||
'unicodePwd': [(ldap3.MODIFY_REPLACE, [LDAPConnector.encode_pass(new_password)])],
|
||||
}
|
||||
return self._do_modify(diff, **fields)
|
||||
|
||||
def add_to_group(self, group_dn, **fields):
|
||||
"""Adds mail or user_dn to group_dn
|
||||
Returns True on success, otherwise False"""
|
||||
user_dn = self.lookup(**fields)
|
||||
diff = {
|
||||
'member': [(ldap3.MODIFY_ADD), [user_dn]]
|
||||
}
|
||||
return self._do_modify(diff, user_dn=group_dn)
|
||||
|
||||
def remove_from_group(self, group_dn, **fields):
|
||||
"""Removes mail or user_dn from group_dn
|
||||
Returns True on success, otherwise False"""
|
||||
user_dn = self.lookup(**fields)
|
||||
diff = {
|
||||
'member': [(ldap3.MODIFY_DELETE), [user_dn]]
|
||||
}
|
||||
return self._do_modify(diff, user_dn=group_dn)
|
||||
48
passbook/sources/ldap/migrations/0001_initial.py
Normal file
48
passbook/sources/ldap/migrations/0001_initial.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LDAPSource',
|
||||
fields=[
|
||||
('source_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Source')),
|
||||
('server_uri', models.TextField()),
|
||||
('bind_cn', models.TextField()),
|
||||
('bind_password', models.TextField()),
|
||||
('type', models.CharField(choices=[('ad', 'Active Directory'), ('generic', 'Generic')], max_length=20)),
|
||||
('domain', models.TextField()),
|
||||
('base_dn', models.TextField()),
|
||||
('create_user', models.BooleanField(default=False)),
|
||||
('reset_password', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'LDAP Source',
|
||||
'verbose_name_plural': 'LDAP Sources',
|
||||
},
|
||||
bases=('passbook_core.source',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LDAPGroupMembershipPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('dn', models.TextField()),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_sources_ldap.LDAPSource')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'LDAP Group Membership Policy',
|
||||
'verbose_name_plural': 'LDAP Group Membership Policys',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
]
|
||||
0
passbook/sources/ldap/migrations/__init__.py
Normal file
0
passbook/sources/ldap/migrations/__init__.py
Normal file
55
passbook/sources/ldap/models.py
Normal file
55
passbook/sources/ldap/models.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""passbook LDAP Models"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy, Source, User
|
||||
|
||||
|
||||
class LDAPSource(Source):
|
||||
"""LDAP Authentication source"""
|
||||
|
||||
TYPE_ACTIVE_DIRECTORY = 'ad'
|
||||
TYPE_GENERIC = 'generic'
|
||||
TYPES = (
|
||||
(TYPE_ACTIVE_DIRECTORY, _('Active Directory')),
|
||||
(TYPE_GENERIC, _('Generic')),
|
||||
)
|
||||
|
||||
server_uri = models.TextField()
|
||||
bind_cn = models.TextField()
|
||||
bind_password = models.TextField()
|
||||
type = models.CharField(max_length=20, choices=TYPES)
|
||||
|
||||
domain = models.TextField()
|
||||
base_dn = models.TextField()
|
||||
create_user = models.BooleanField(default=False)
|
||||
reset_password = models.BooleanField(default=True)
|
||||
|
||||
form = 'passbook.sources.ldap.forms.LDAPSourceForm'
|
||||
|
||||
@property
|
||||
def get_login_button(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('LDAP Source')
|
||||
verbose_name_plural = _('LDAP Sources')
|
||||
|
||||
class LDAPGroupMembershipPolicy(Policy):
|
||||
"""Policy to check if a user is in a certain LDAP Group"""
|
||||
|
||||
dn = models.TextField()
|
||||
source = models.ForeignKey('LDAPSource', on_delete=models.CASCADE)
|
||||
|
||||
form = 'passbook.sources.ldap.forms.LDAPGroupMembershipPolicyForm'
|
||||
|
||||
def passes(self, user: User):
|
||||
"""Check if user instance passes this policy"""
|
||||
raise NotImplementedError()
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('LDAP Group Membership Policy')
|
||||
verbose_name_plural = _('LDAP Group Membership Policys')
|
||||
5
passbook/sources/ldap/settings.py
Normal file
5
passbook/sources/ldap/settings.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""LDAP Settings"""
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'passbook.sources.ldap.auth.LDAPBackend',
|
||||
]
|
||||
33
passbook/sources/ldap/templates/ldap/settings.html
Normal file
33
passbook/sources/ldap/templates/ldap/settings.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "_admin/module_default.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utils %}
|
||||
|
||||
{% block title %}
|
||||
{% title "Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block module_content %}
|
||||
<h2><clr-icon shape="application" size="32"></clr-icon>{% trans 'LDAP connection' %}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<form role="form" method="POST">
|
||||
<div class="card-block">
|
||||
<h3><clr-icon shape="cog" size="32"></clr-icon>{% trans 'General settings' %}</h3>
|
||||
{% include 'partials/form.html' with form=general %}
|
||||
<h3><clr-icon shape="connect" size="32"></clr-icon>{% trans 'Connection settings' %}</h3>
|
||||
{% include 'partials/form.html' with form=connection %}
|
||||
<h3><clr-icon shape="certificate" size="32"></clr-icon>{% trans 'Authentication backend ' %}</h3>
|
||||
{% include 'partials/form.html' with form=authentication %}
|
||||
<h3><clr-icon shape="users" size="32"></clr-icon>{% trans 'Create users settings' %}</h3>
|
||||
{% include 'partials/form.html' with form=create_users %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" value="general" class="btn btn-sm btn-primary">{% trans 'Update' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
passbook/sources/ldap/urls.py
Normal file
9
passbook/sources/ldap/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
# """passbook LDAP Urls"""
|
||||
|
||||
# from django.conf.urls import url
|
||||
|
||||
# from passbook.mod.auth.ldap import views
|
||||
|
||||
# urlpatterns = [
|
||||
# url(r'^settings/$', views.admin_settings, name='admin_settings'),
|
||||
# ]
|
||||
38
passbook/sources/ldap/views.py
Normal file
38
passbook/sources/ldap/views.py
Normal file
@ -0,0 +1,38 @@
|
||||
# """passbook LDAP Views"""
|
||||
|
||||
|
||||
# from django.contrib import messages
|
||||
# from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
# from django.http import HttpRequest, HttpResponse
|
||||
# from django.shortcuts import redirect, render
|
||||
# from django.urls import reverse
|
||||
# from django.utils.translation import ugettext as _
|
||||
|
||||
# from passbook.sources.ldap.forms import (AuthenticationBackendSettings,
|
||||
# ConnectionSettings,
|
||||
# CreateUsersSettings,
|
||||
# GeneralSettingsForm)
|
||||
|
||||
|
||||
# @login_required
|
||||
# @user_passes_test(lambda u: u.is_superuser)
|
||||
# def admin_settings(request: HttpRequest) -> HttpResponse:
|
||||
# """Default view for modules without admin view"""
|
||||
# form_classes = {
|
||||
# 'general': GeneralSettingsForm,
|
||||
# 'connection': ConnectionSettings,
|
||||
# 'authentication': AuthenticationBackendSettings,
|
||||
# 'create_users': CreateUsersSettings,
|
||||
# }
|
||||
# render_data = {}
|
||||
# for form_key, form_class in form_classes.items():
|
||||
# render_data[form_key] = form_class(request.POST if request.method == 'POST' else None)
|
||||
# if request.method == 'POST':
|
||||
# update_count = 0
|
||||
# for form_key, form_class in form_classes.items():
|
||||
# form = form_class(request.POST)
|
||||
# if form.is_valid():
|
||||
# update_count += form.save()
|
||||
# messages.success(request, _('Successfully updated %d settings.' % update_count))
|
||||
# return redirect(reverse('passbook_ldap:admin_settings'))
|
||||
# return render(request, 'ldap/settings.html', render_data)
|
||||
0
passbook/sources/oauth/__init__.py
Normal file
0
passbook/sources/oauth/__init__.py
Normal file
5
passbook/sources/oauth/admin.py
Normal file
5
passbook/sources/oauth/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""passbook oauth_client admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_sources_oauth')
|
||||
25
passbook/sources/oauth/apps.py
Normal file
25
passbook/sources/oauth/apps.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""passbook oauth_client config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from structlog import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
class PassbookSourceOAuthConfig(AppConfig):
|
||||
"""passbook source.oauth config"""
|
||||
|
||||
name = 'passbook.sources.oauth'
|
||||
label = 'passbook_sources_oauth'
|
||||
verbose_name = 'passbook Sources.OAuth'
|
||||
mountpoint = 'source/oauth/'
|
||||
|
||||
def ready(self):
|
||||
"""Load source_types from config file"""
|
||||
for source_type in settings.PASSBOOK_SOURCES_OAUTH_TYPES:
|
||||
try:
|
||||
import_module(source_type)
|
||||
LOGGER.info("Loaded source_type", source_class=source_type)
|
||||
except ImportError as exc:
|
||||
LOGGER.debug(exc)
|
||||
25
passbook/sources/oauth/backends.py
Normal file
25
passbook/sources/oauth/backends.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""passbook oauth_client Authorization backend"""
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db.models import Q
|
||||
|
||||
from passbook.sources.oauth.models import (OAuthSource,
|
||||
UserOAuthSourceConnection)
|
||||
|
||||
|
||||
class AuthorizedServiceBackend(ModelBackend):
|
||||
"Authentication backend for users registered with remote OAuth provider."
|
||||
|
||||
def authenticate(self, request, source=None, identifier=None):
|
||||
"Fetch user for a given source by id."
|
||||
source_q = Q(source__name=source)
|
||||
if isinstance(source, OAuthSource):
|
||||
source_q = Q(source=source)
|
||||
try:
|
||||
access = UserOAuthSourceConnection.objects.filter(
|
||||
source_q, identifier=identifier
|
||||
).select_related('user')[0]
|
||||
except IndexError:
|
||||
return None
|
||||
else:
|
||||
return access.user
|
||||
245
passbook/sources/oauth/clients.py
Normal file
245
passbook/sources/oauth/clients.py
Normal file
@ -0,0 +1,245 @@
|
||||
"""OAuth Clients"""
|
||||
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import constant_time_compare, get_random_string
|
||||
from django.utils.encoding import force_text
|
||||
from requests import Session
|
||||
from requests.exceptions import RequestException
|
||||
from requests_oauthlib import OAuth1
|
||||
from structlog import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class BaseOAuthClient:
|
||||
"""Base OAuth Client"""
|
||||
|
||||
_session = None
|
||||
|
||||
def __init__(self, source, token=''): # nosec
|
||||
self.source = source
|
||||
self.token = token
|
||||
self._session = Session()
|
||||
self._session.headers.update({'User-Agent': 'web:passbook:%s' % settings.VERSION})
|
||||
|
||||
def get_access_token(self, request, callback=None):
|
||||
"Fetch access token from callback request."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
response = self.request('get', self.source.profile_url, token=raw_token)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def get_redirect_url(self, request, callback, parameters=None):
|
||||
"Build authentication redirect url."
|
||||
args = self.get_redirect_args(request, callback=callback)
|
||||
additional = parameters or {}
|
||||
args.update(additional)
|
||||
params = urlencode(args)
|
||||
LOGGER.info("Redirect args: %s", args)
|
||||
return '{0}?{1}'.format(self.source.authorization_url, params)
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request."
|
||||
return self._session.request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
"""
|
||||
Return Session Key
|
||||
"""
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
|
||||
class OAuthClient(BaseOAuthClient):
|
||||
"""OAuth1 Client"""
|
||||
|
||||
def get_access_token(self, request, callback=None):
|
||||
"Fetch access token from callback request."
|
||||
raw_token = request.session.get(self.session_key, None)
|
||||
verifier = request.GET.get('oauth_verifier', None)
|
||||
if raw_token is not None and verifier is not None:
|
||||
data = {'oauth_verifier': verifier}
|
||||
callback = request.build_absolute_uri(callback or request.path)
|
||||
callback = force_text(callback)
|
||||
try:
|
||||
response = self.request('post', self.source.access_token_url,
|
||||
token=raw_token, data=data, oauth_callback=callback)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch access token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
return None
|
||||
|
||||
def get_request_token(self, request, callback):
|
||||
"Fetch the OAuth request token. Only required for OAuth 1.0."
|
||||
callback = force_text(request.build_absolute_uri(callback))
|
||||
try:
|
||||
response = self.request(
|
||||
'post', self.source.request_token_url, oauth_callback=callback)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch request token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
callback = force_text(request.build_absolute_uri(callback))
|
||||
raw_token = self.get_request_token(request, callback)
|
||||
token, secret = self.parse_raw_token(raw_token)
|
||||
if token is not None and secret is not None:
|
||||
request.session[self.session_key] = raw_token
|
||||
return {
|
||||
'oauth_token': token,
|
||||
'oauth_callback': callback,
|
||||
}
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
if raw_token is None:
|
||||
return (None, None)
|
||||
query_string = parse_qs(raw_token)
|
||||
token = query_string.get('oauth_token', [None])[0]
|
||||
secret = query_string.get('oauth_token_secret', [None])[0]
|
||||
return (token, secret)
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request. Constructs necessary auth."
|
||||
user_token = kwargs.pop('token', self.token)
|
||||
token, secret = self.parse_raw_token(user_token)
|
||||
callback = kwargs.pop('oauth_callback', None)
|
||||
verifier = kwargs.get('data', {}).pop('oauth_verifier', None)
|
||||
oauth = OAuth1(
|
||||
resource_owner_key=token,
|
||||
resource_owner_secret=secret,
|
||||
client_key=self.source.consumer_key,
|
||||
client_secret=self.source.consumer_secret,
|
||||
verifier=verifier,
|
||||
callback_uri=callback,
|
||||
)
|
||||
kwargs['auth'] = oauth
|
||||
return super(OAuthClient, self).request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
return 'oauth-client-{0}-request-token'.format(self.source.name)
|
||||
|
||||
|
||||
class OAuth2Client(BaseOAuthClient):
|
||||
"""OAuth2 Client"""
|
||||
|
||||
def check_application_state(self, request, callback):
|
||||
"Check optional state parameter."
|
||||
stored = request.session.get(self.session_key, None)
|
||||
returned = request.GET.get('state', None)
|
||||
check = False
|
||||
if stored is not None:
|
||||
if returned is not None:
|
||||
check = constant_time_compare(stored, returned)
|
||||
else:
|
||||
LOGGER.warning('No state parameter returned by the source.')
|
||||
else:
|
||||
LOGGER.warning('No state stored in the sesssion.')
|
||||
return check
|
||||
|
||||
def get_access_token(self, request, callback=None, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
callback = request.build_absolute_uri(callback or request.path)
|
||||
if not self.check_application_state(request, callback):
|
||||
LOGGER.warning('Application state check failed.')
|
||||
return None
|
||||
if 'code' in request.GET:
|
||||
args = {
|
||||
'client_id': self.source.consumer_key,
|
||||
'redirect_uri': callback,
|
||||
'client_secret': self.source.consumer_secret,
|
||||
'code': request.GET['code'],
|
||||
'grant_type': 'authorization_code',
|
||||
}
|
||||
else:
|
||||
LOGGER.warning('No code returned by the source')
|
||||
return None
|
||||
try:
|
||||
response = self.request('post', self.source.access_token_url,
|
||||
data=args, **request_kwargs)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch access token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
|
||||
def get_application_state(self, request, callback):
|
||||
"Generate state optional parameter."
|
||||
return get_random_string(32)
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
callback = request.build_absolute_uri(callback)
|
||||
args = {
|
||||
'client_id': self.source.consumer_key,
|
||||
'redirect_uri': callback,
|
||||
'response_type': 'code',
|
||||
}
|
||||
state = self.get_application_state(request, callback)
|
||||
if state is not None:
|
||||
args['state'] = state
|
||||
request.session[self.session_key] = state
|
||||
return args
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
if raw_token is None:
|
||||
return (None, None)
|
||||
# Load as json first then parse as query string
|
||||
try:
|
||||
token_data = json.loads(raw_token)
|
||||
except ValueError:
|
||||
token = parse_qs(raw_token).get('access_token', [None])[0]
|
||||
else:
|
||||
token = token_data.get('access_token', None)
|
||||
return (token, None)
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request. Constructs necessary auth."
|
||||
user_token = kwargs.pop('token', self.token)
|
||||
token, _ = self.parse_raw_token(user_token)
|
||||
if token is not None:
|
||||
params = kwargs.get('params', {})
|
||||
params['access_token'] = token
|
||||
kwargs['params'] = params
|
||||
return super(OAuth2Client, self).request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
return 'oauth-client-{0}-request-state'.format(self.source.name)
|
||||
|
||||
|
||||
def get_client(source, token=''): # nosec
|
||||
"Return the API client for the given source."
|
||||
cls = OAuth2Client
|
||||
if source.request_token_url:
|
||||
cls = OAuthClient
|
||||
return cls(source, token)
|
||||
124
passbook/sources/oauth/forms.py
Normal file
124
passbook/sources/oauth/forms.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""passbook oauth_client forms"""
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
from passbook.sources.oauth.types.manager import MANAGER
|
||||
|
||||
|
||||
class OAuthSourceForm(forms.ModelForm):
|
||||
"""OAuthSource Form"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if hasattr(self.Meta, 'overrides'):
|
||||
for overide_field, overide_value in getattr(self.Meta, 'overrides').items():
|
||||
self.fields[overide_field].initial = overide_value
|
||||
self.fields[overide_field].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OAuthSource
|
||||
fields = SOURCE_FORM_FIELDS + ['provider_type', 'request_token_url', 'authorization_url',
|
||||
'access_token_url', 'profile_url', 'consumer_key',
|
||||
'consumer_secret']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'consumer_key': forms.TextInput(),
|
||||
'consumer_secret': forms.TextInput(),
|
||||
'provider_type': forms.Select(choices=MANAGER.get_name_tuple()),
|
||||
'policies': FilteredSelectMultiple(_('policies'), False)
|
||||
}
|
||||
labels = {
|
||||
'request_token_url': _('Request Token URL'),
|
||||
'authorization_url': _('Authorization URL'),
|
||||
'access_token_url': _('Access Token URL'),
|
||||
'profile_url': _('Profile URL'),
|
||||
}
|
||||
|
||||
|
||||
class GitHubOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for GitHub"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'github',
|
||||
'request_token_url': '',
|
||||
'authorization_url': 'https://github.com/login/oauth/authorize',
|
||||
'access_token_url': 'https://github.com/login/oauth/access_token',
|
||||
'profile_url': ' https://api.github.com/user',
|
||||
}
|
||||
|
||||
|
||||
class TwitterOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Twitter"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'twitter',
|
||||
'request_token_url': 'https://api.twitter.com/oauth/request_token',
|
||||
'authorization_url': 'https://api.twitter.com/oauth/authenticate',
|
||||
'access_token_url': 'https://api.twitter.com/oauth/access_token',
|
||||
'profile_url': ' https://api.twitter.com/1.1/account/verify_credentials.json',
|
||||
}
|
||||
|
||||
|
||||
class FacebookOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Facebook"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'facebook',
|
||||
'request_token_url': '',
|
||||
'authorization_url': 'https://www.facebook.com/v2.8/dialog/oauth',
|
||||
'access_token_url': 'https://graph.facebook.com/v2.8/oauth/access_token',
|
||||
'profile_url': ' https://graph.facebook.com/v2.8/me?fields=name,email,short_name',
|
||||
}
|
||||
|
||||
|
||||
class DiscordOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Discord"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'discord',
|
||||
'request_token_url': '',
|
||||
'authorization_url': 'https://discordapp.com/api/oauth2/authorize',
|
||||
'access_token_url': 'https://discordapp.com/api/oauth2/token',
|
||||
'profile_url': ' https://discordapp.com/api/users/@me',
|
||||
}
|
||||
|
||||
|
||||
class GoogleOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Google"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'google',
|
||||
'request_token_url': '',
|
||||
'authorization_url': 'https://accounts.google.com/o/oauth2/auth',
|
||||
'access_token_url': 'https://accounts.google.com/o/oauth2/token',
|
||||
'profile_url': ' https://www.googleapis.com/oauth2/v1/userinfo',
|
||||
}
|
||||
|
||||
|
||||
class AzureADOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for AzureAD"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
|
||||
overrides = {
|
||||
'provider_type': 'azure_ad',
|
||||
'request_token_url': '',
|
||||
'authorization_url': 'https://login.microsoftonline.com/common/oauth2/authorize',
|
||||
'access_token_url': 'https://login.microsoftonline.com/common/oauth2/token',
|
||||
'profile_url': ' https://graph.windows.net/myorganization/me?api-version=1.6',
|
||||
}
|
||||
80
passbook/sources/oauth/locale/de/LC_MESSAGES/django.po
Normal file
80
passbook/sources/oauth/locale/de/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
||||
79
passbook/sources/oauth/locale/en/LC_MESSAGES/django.po
Normal file
79
passbook/sources/oauth/locale/en/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,79 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-20 10:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
||||
80
passbook/sources/oauth/locale/es/LC_MESSAGES/django.po
Normal file
80
passbook/sources/oauth/locale/es/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
||||
80
passbook/sources/oauth/locale/fr/LC_MESSAGES/django.po
Normal file
80
passbook/sources/oauth/locale/fr/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
||||
47
passbook/sources/oauth/migrations/0001_initial.py
Normal file
47
passbook/sources/oauth/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OAuthSource',
|
||||
fields=[
|
||||
('source_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Source')),
|
||||
('provider_type', models.CharField(max_length=255)),
|
||||
('request_token_url', models.CharField(blank=True, max_length=255)),
|
||||
('authorization_url', models.CharField(max_length=255)),
|
||||
('access_token_url', models.CharField(max_length=255)),
|
||||
('profile_url', models.CharField(max_length=255)),
|
||||
('consumer_key', models.TextField()),
|
||||
('consumer_secret', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Generic OAuth Source',
|
||||
'verbose_name_plural': 'Generic OAuth Sources',
|
||||
},
|
||||
bases=('passbook_core.source',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserOAuthSourceConnection',
|
||||
fields=[
|
||||
('usersourceconnection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.UserSourceConnection')),
|
||||
('identifier', models.CharField(max_length=255)),
|
||||
('access_token', models.TextField(blank=True, default=None, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User OAuth Source Connection',
|
||||
'verbose_name_plural': 'User OAuth Source Connections',
|
||||
},
|
||||
bases=('passbook_core.usersourceconnection',),
|
||||
),
|
||||
]
|
||||
0
passbook/sources/oauth/migrations/__init__.py
Normal file
0
passbook/sources/oauth/migrations/__init__.py
Normal file
149
passbook/sources/oauth/models.py
Normal file
149
passbook/sources/oauth/models.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""OAuth Client models"""
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Source, UserSourceConnection
|
||||
from passbook.sources.oauth.clients import get_client
|
||||
|
||||
|
||||
class OAuthSource(Source):
|
||||
"""Configuration for OAuth provider."""
|
||||
|
||||
provider_type = models.CharField(max_length=255)
|
||||
request_token_url = models.CharField(blank=True, max_length=255)
|
||||
authorization_url = models.CharField(max_length=255)
|
||||
access_token_url = models.CharField(max_length=255)
|
||||
profile_url = models.CharField(max_length=255)
|
||||
consumer_key = models.TextField()
|
||||
consumer_secret = models.TextField()
|
||||
|
||||
form = 'passbook.sources.oauth.forms.OAuthSourceForm'
|
||||
|
||||
@property
|
||||
def is_link(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def get_login_button(self):
|
||||
url = reverse_lazy('passbook_sources_oauth:oauth-client-login',
|
||||
kwargs={'source_slug': self.slug})
|
||||
return url, self.provider_type, self.name
|
||||
|
||||
@property
|
||||
def additional_info(self):
|
||||
return "Callback URL: <pre>%s</pre>" % \
|
||||
reverse_lazy('passbook_sources_oauth:oauth-client-callback',
|
||||
kwargs={'source_slug': self.slug})
|
||||
|
||||
def has_user_settings(self):
|
||||
"""Entrypoint to integrate with User settings. Can either return False if no
|
||||
user settings are available, or a tuple or string, string, string where the first string
|
||||
is the name the item has, the second string is the icon and the third is the view-name."""
|
||||
icon_type = self.provider_type
|
||||
if icon_type == 'azure ad':
|
||||
icon_type = 'windows'
|
||||
icon_class = 'fa fa-%s' % icon_type
|
||||
view_name = 'passbook_sources_oauth:oauth-client-user'
|
||||
return self.name, icon_class, reverse((view_name), kwargs={
|
||||
'source_slug': self.slug
|
||||
})
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Generic OAuth Source')
|
||||
verbose_name_plural = _('Generic OAuth Sources')
|
||||
|
||||
|
||||
class GitHubOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify GitHub Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.GitHubOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('GitHub OAuth Source')
|
||||
verbose_name_plural = _('GitHub OAuth Sources')
|
||||
|
||||
|
||||
class TwitterOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify Twitter Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.TwitterOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Twitter OAuth Source')
|
||||
verbose_name_plural = _('Twitter OAuth Sources')
|
||||
|
||||
|
||||
class FacebookOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify Facebook Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.FacebookOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Facebook OAuth Source')
|
||||
verbose_name_plural = _('Facebook OAuth Sources')
|
||||
|
||||
|
||||
class DiscordOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify Discord Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.DiscordOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Discord OAuth Source')
|
||||
verbose_name_plural = _('Discord OAuth Sources')
|
||||
|
||||
|
||||
class GoogleOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify Google Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.GoogleOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Google OAuth Source')
|
||||
verbose_name_plural = _('Google OAuth Sources')
|
||||
|
||||
|
||||
class AzureADOAuthSource(OAuthSource):
|
||||
"""Abstract subclass of OAuthSource to specify AzureAD Form"""
|
||||
|
||||
form = 'passbook.sources.oauth.forms.AzureADOAuthSourceForm'
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Azure AD OAuth Source')
|
||||
verbose_name_plural = _('Azure AD OAuth Sources')
|
||||
|
||||
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
identifier = models.CharField(max_length=255)
|
||||
access_token = models.TextField(blank=True, null=True, default=None)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.access_token = self.access_token or None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def api_client(self):
|
||||
"""Get API Client"""
|
||||
return get_client(self.source, self.access_token or '')
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('User OAuth Source Connection')
|
||||
verbose_name_plural = _('User OAuth Source Connections')
|
||||
16
passbook/sources/oauth/settings.py
Normal file
16
passbook/sources/oauth/settings.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Oauth2 Client Settings"""
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'passbook.sources.oauth.backends.AuthorizedServiceBackend',
|
||||
]
|
||||
|
||||
PASSBOOK_SOURCES_OAUTH_TYPES = [
|
||||
'passbook.sources.oauth.types.discord',
|
||||
'passbook.sources.oauth.types.facebook',
|
||||
'passbook.sources.oauth.types.github',
|
||||
'passbook.sources.oauth.types.google',
|
||||
'passbook.sources.oauth.types.reddit',
|
||||
'passbook.sources.oauth.types.supervisr',
|
||||
'passbook.sources.oauth.types.twitter',
|
||||
'passbook.sources.oauth.types.azure_ad',
|
||||
]
|
||||
18
passbook/sources/oauth/templates/oauth_client/user.html
Normal file
18
passbook/sources/oauth/templates/oauth_client/user.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page %}
|
||||
<h1>{{ source.name }}</h1>
|
||||
{% if connections.exists %}
|
||||
<p>{% trans 'Connected.' %}</p>
|
||||
<a class="btn btn-danger" href="{% url 'passbook_oauth_client:oauth-client-disconnect' source_slug=source.slug %}">
|
||||
{% trans 'Disconnect' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p>Not connected.</p>
|
||||
<a class="btn btn-primary" href="{% url 'passbook_oauth_client:oauth-client-login' source_slug=source.slug %}">
|
||||
{% trans 'Connect' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
0
passbook/sources/oauth/types/__init__.py
Normal file
0
passbook/sources/oauth/types/__init__.py
Normal file
52
passbook/sources/oauth/types/azure_ad.py
Normal file
52
passbook/sources/oauth/types/azure_ad.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""AzureAD OAuth2 Views"""
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.clients import OAuth2Client
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AzureADOAuth2Client(OAuth2Client):
|
||||
"""AzureAD OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)['access_token']
|
||||
headers = {
|
||||
'Authorization': 'Bearer %s' % token
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Azure AD')
|
||||
class AzureADOAuthCallback(OAuthCallback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
client_class = AzureADOAuth2Client
|
||||
|
||||
def get_user_id(self, source, info):
|
||||
return uuid.UUID(info.get('objectId')).int
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('displayName'),
|
||||
'email': info.get('mail', None) or info.get('otherMails')[0],
|
||||
'name': info.get('displayName'),
|
||||
'password': None,
|
||||
}
|
||||
return user_get_or_create(**user_data)
|
||||
59
passbook/sources/oauth/types/discord.py
Normal file
59
passbook/sources/oauth/types/discord.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Discord OAuth Views"""
|
||||
import json
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.clients import OAuth2Client
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='Discord')
|
||||
class DiscordOAuthRedirect(OAuthRedirect):
|
||||
"""Discord OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email identify',
|
||||
}
|
||||
|
||||
|
||||
class DiscordOAuth2Client(OAuth2Client):
|
||||
"""Discord OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)
|
||||
headers = {
|
||||
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Discord')
|
||||
class DiscordOAuth2Callback(OAuthCallback):
|
||||
"""Discord OAuth2 Callback"""
|
||||
|
||||
client_class = DiscordOAuth2Client
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('username'),
|
||||
'email': info.get('email', 'None'),
|
||||
'name': info.get('username'),
|
||||
'password': None,
|
||||
}
|
||||
discord_user = user_get_or_create(**user_data)
|
||||
return discord_user
|
||||
30
passbook/sources/oauth/types/facebook.py
Normal file
30
passbook/sources/oauth/types/facebook.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Facebook OAuth Views"""
|
||||
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='Facebook')
|
||||
class FacebookOAuthRedirect(OAuthRedirect):
|
||||
"""Facebook OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email',
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Facebook')
|
||||
class FacebookOAuth2Callback(OAuthCallback):
|
||||
"""Facebook OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('name'),
|
||||
'email': info.get('email', ''),
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
fb_user = user_get_or_create(**user_data)
|
||||
return fb_user
|
||||
20
passbook/sources/oauth/types/github.py
Normal file
20
passbook/sources/oauth/types/github.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""GitHub OAuth Views"""
|
||||
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='GitHub')
|
||||
class GitHubOAuth2Callback(OAuthCallback):
|
||||
"""GitHub OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('login'),
|
||||
'email': info.get('email', ''),
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
gh_user = user_get_or_create(**user_data)
|
||||
return gh_user
|
||||
29
passbook/sources/oauth/types/google.py
Normal file
29
passbook/sources/oauth/types/google.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Google OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='Google')
|
||||
class GoogleOAuthRedirect(OAuthRedirect):
|
||||
"""Google OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email profile',
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Google')
|
||||
class GoogleOAuth2Callback(OAuthCallback):
|
||||
"""Google OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('email'),
|
||||
'email': info.get('email', ''),
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
google_user = user_get_or_create(**user_data)
|
||||
return google_user
|
||||
51
passbook/sources/oauth/types/manager.py
Normal file
51
passbook/sources/oauth/types/manager.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Source type manager"""
|
||||
from enum import Enum
|
||||
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
class RequestKind(Enum):
|
||||
"""Enum of OAuth Request types"""
|
||||
|
||||
callback = 'callback'
|
||||
redirect = 'redirect'
|
||||
|
||||
|
||||
class SourceTypeManager:
|
||||
"""Manager to hold all Source types."""
|
||||
|
||||
__source_types = {}
|
||||
__names = []
|
||||
|
||||
def source(self, kind, name):
|
||||
"""Class decorator to register classes inline."""
|
||||
def inner_wrapper(cls):
|
||||
if kind not in self.__source_types:
|
||||
self.__source_types[kind] = {}
|
||||
self.__source_types[kind][name.lower()] = cls
|
||||
self.__names.append(name)
|
||||
LOGGER.debug("Registered source", source_class=cls.__name__, kind=kind)
|
||||
return cls
|
||||
return inner_wrapper
|
||||
|
||||
def get_name_tuple(self):
|
||||
"""Get list of tuples of all registered names"""
|
||||
return [(x.lower(), x) for x in set(self.__names)]
|
||||
|
||||
def find(self, source, kind):
|
||||
"""Find fitting Source Type"""
|
||||
if kind in self.__source_types:
|
||||
if source.provider_type in self.__source_types[kind]:
|
||||
return self.__source_types[kind][source.provider_type]
|
||||
# Return defaults
|
||||
if kind == RequestKind.callback:
|
||||
return OAuthCallback
|
||||
if kind == RequestKind.redirect:
|
||||
return OAuthRedirect
|
||||
raise KeyError
|
||||
|
||||
|
||||
MANAGER = SourceTypeManager()
|
||||
68
passbook/sources/oauth/types/reddit.py
Normal file
68
passbook/sources/oauth/types/reddit.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Reddit OAuth Views"""
|
||||
import json
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests.exceptions import RequestException
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.clients import OAuth2Client
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='reddit')
|
||||
class RedditOAuthRedirect(OAuthRedirect):
|
||||
"""Reddit OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'identity',
|
||||
'duration': 'permanent',
|
||||
}
|
||||
|
||||
|
||||
class RedditOAuth2Client(OAuth2Client):
|
||||
"""Reddit OAuth2 Client"""
|
||||
|
||||
def get_access_token(self, request, callback=None, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
auth = HTTPBasicAuth(
|
||||
self.source.consumer_key,
|
||||
self.source.consumer_secret)
|
||||
return super(RedditOAuth2Client, self).get_access_token(request, callback, auth=auth)
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)
|
||||
headers = {
|
||||
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='reddit')
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
"""Reddit OAuth2 Callback"""
|
||||
|
||||
client_class = RedditOAuth2Client
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('name'),
|
||||
'email': None,
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
reddit_user = user_get_or_create(**user_data)
|
||||
return reddit_user
|
||||
53
passbook/sources/oauth/types/supervisr.py
Normal file
53
passbook/sources/oauth/types/supervisr.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Supervisr OAuth2 Views"""
|
||||
|
||||
import json
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.clients import OAuth2Client
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class SupervisrOAuth2Client(OAuth2Client):
|
||||
"""Supervisr OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)['access_token']
|
||||
headers = {
|
||||
'Authorization': 'Bearer:%s' % token
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=raw_token, headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='supervisr')
|
||||
class SupervisrOAuthCallback(OAuthCallback):
|
||||
"""Supervisr OAuth2 Callback"""
|
||||
|
||||
client_class = SupervisrOAuth2Client
|
||||
|
||||
def get_user_id(self, source, info):
|
||||
return info['pk']
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('username'),
|
||||
'email': info.get('email', ''),
|
||||
'name': info.get('first_name'),
|
||||
'password': None,
|
||||
}
|
||||
sv_user = user_get_or_create(**user_data)
|
||||
return sv_user
|
||||
44
passbook/sources/oauth/types/twitter.py
Normal file
44
passbook/sources/oauth/types/twitter.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Twitter OAuth Views"""
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.clients import OAuthClient
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TwitterOAuthClient(OAuthClient):
|
||||
"""Twitter OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
response = self.request('get', self.source.profile_url + "?include_email=true",
|
||||
token=raw_token)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Twitter')
|
||||
class TwitterOAuthCallback(OAuthCallback):
|
||||
"""Twitter OAuth2 Callback"""
|
||||
|
||||
client_class = TwitterOAuthClient
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('screen_name'),
|
||||
'email': info.get('email', ''),
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
tw_user = user_get_or_create(**user_data)
|
||||
return tw_user
|
||||
17
passbook/sources/oauth/urls.py
Normal file
17
passbook/sources/oauth/urls.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""passbook oauth_client urls"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from passbook.sources.oauth.types.manager import RequestKind
|
||||
from passbook.sources.oauth.views import core, dispatcher, user
|
||||
|
||||
urlpatterns = [
|
||||
path('login/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
|
||||
kind=RequestKind.redirect), name='oauth-client-login'),
|
||||
path('callback/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
|
||||
kind=RequestKind.callback), name='oauth-client-callback'),
|
||||
path('disconnect/<slug:source_slug>/', core.DisconnectView.as_view(),
|
||||
name='oauth-client-disconnect'),
|
||||
path('user/<slug:source_slug>/', user.UserSettingsView.as_view(),
|
||||
name='oauth-client-user'),
|
||||
]
|
||||
17
passbook/sources/oauth/utils.py
Normal file
17
passbook/sources/oauth/utils.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""OAuth Client User Creation Utils"""
|
||||
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
def user_get_or_create(**kwargs):
|
||||
"""Create user or return existing user"""
|
||||
try:
|
||||
new_user = User.objects.create_user(**kwargs)
|
||||
except IntegrityError:
|
||||
# At this point we've already checked that there is no existing connection
|
||||
# to any user. Hence if we can't create the user,
|
||||
kwargs['username'] = '%s_1' % kwargs['username']
|
||||
new_user = User.objects.create_user(**kwargs)
|
||||
return new_user
|
||||
0
passbook/sources/oauth/views/__init__.py
Normal file
0
passbook/sources/oauth/views/__init__.py
Normal file
240
passbook/sources/oauth/views/core.py
Normal file
240
passbook/sources/oauth/views/core.py
Normal file
@ -0,0 +1,240 @@
|
||||
"""Core OAauth Views"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import RedirectView, View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
||||
from passbook.lib.utils.reflection import app
|
||||
from passbook.sources.oauth.clients import get_client
|
||||
from passbook.sources.oauth.models import (OAuthSource,
|
||||
UserOAuthSourceConnection)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class OAuthClientMixin:
|
||||
"Mixin for getting OAuth client for a source."
|
||||
|
||||
client_class = None
|
||||
|
||||
def get_client(self, source):
|
||||
"Get instance of the OAuth client for this source."
|
||||
if self.client_class is not None:
|
||||
# pylint: disable=not-callable
|
||||
return self.client_class(source)
|
||||
return get_client(source)
|
||||
|
||||
|
||||
class OAuthRedirect(OAuthClientMixin, RedirectView):
|
||||
"Redirect user to OAuth source to enable access."
|
||||
|
||||
permanent = False
|
||||
params = None
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_additional_parameters(self, source):
|
||||
"Return additional redirect parameters for this source."
|
||||
return self.params or {}
|
||||
|
||||
def get_callback_url(self, source):
|
||||
"Return the callback url for this source."
|
||||
return reverse('passbook_oauth_client:oauth-client-callback',
|
||||
kwargs={'source_slug': source.slug})
|
||||
|
||||
def get_redirect_url(self, **kwargs):
|
||||
"Build redirect url for a given source."
|
||||
slug = kwargs.get('source_slug', '')
|
||||
try:
|
||||
source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
else:
|
||||
if not source.enabled:
|
||||
raise Http404('source %s is not enabled.' % slug)
|
||||
client = self.get_client(source)
|
||||
callback = self.get_callback_url(source)
|
||||
params = self.get_additional_parameters(source)
|
||||
return client.get_redirect_url(self.request, callback=callback, parameters=params)
|
||||
|
||||
|
||||
class OAuthCallback(OAuthClientMixin, View):
|
||||
"Base OAuth callback view."
|
||||
|
||||
source_id = None
|
||||
source = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""View Get handler"""
|
||||
slug = kwargs.get('source_slug', '')
|
||||
try:
|
||||
self.source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
else:
|
||||
if not self.source.enabled:
|
||||
raise Http404('source %s is not enabled.' % slug)
|
||||
client = self.get_client(self.source)
|
||||
callback = self.get_callback_url(self.source)
|
||||
# Fetch access token
|
||||
raw_token = client.get_access_token(self.request, callback=callback)
|
||||
if raw_token is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve token.")
|
||||
# Fetch profile info
|
||||
info = client.get_profile_info(raw_token)
|
||||
if info is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve profile.")
|
||||
identifier = self.get_user_id(self.source, info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure(self.source, "Could not determine id.")
|
||||
# Get or create access record
|
||||
defaults = {
|
||||
'access_token': raw_token,
|
||||
}
|
||||
existing = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source, identifier=identifier)
|
||||
|
||||
if existing.exists():
|
||||
connection = existing.first()
|
||||
connection.access_token = raw_token
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(**defaults)
|
||||
else:
|
||||
connection = UserOAuthSourceConnection(
|
||||
source=self.source,
|
||||
identifier=identifier,
|
||||
access_token=raw_token
|
||||
)
|
||||
user = authenticate(source=self.source, identifier=identifier, request=request)
|
||||
if user is None:
|
||||
LOGGER.debug("Handling new user")
|
||||
return self.handle_new_user(self.source, connection, info)
|
||||
LOGGER.debug("Handling existing user")
|
||||
return self.handle_existing_user(self.source, user, connection, info)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_callback_url(self, source):
|
||||
"Return callback url if different than the current url."
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_error_redirect(self, source, reason):
|
||||
"Return url to redirect on login failure."
|
||||
return settings.LOGIN_URL
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
"Create a shell auth.User."
|
||||
raise NotImplementedError()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_user_id(self, source, info):
|
||||
"Return unique identifier from the profile info."
|
||||
id_key = self.source_id or 'id'
|
||||
result = info
|
||||
try:
|
||||
for key in id_key.split('.'):
|
||||
result = result[key]
|
||||
return result
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def handle_login(self, user, source, access):
|
||||
"""Prepare AuthenticationView, redirect users to remaining Factors"""
|
||||
user = authenticate(source=access.source,
|
||||
identifier=access.identifier, request=self.request)
|
||||
self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk
|
||||
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
|
||||
self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True
|
||||
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_existing_user(self, source, user, access, info):
|
||||
"Login user and redirect."
|
||||
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return self.handle_login(user, source, access)
|
||||
|
||||
def handle_login_failure(self, source, reason):
|
||||
"Message user and redirect on error."
|
||||
LOGGER.warning('Authentication Failure: %s', reason)
|
||||
messages.error(self.request, _('Authentication Failed.'))
|
||||
return redirect(self.get_error_redirect(source, reason))
|
||||
|
||||
def handle_new_user(self, source, access, info):
|
||||
"Create a shell auth.User and redirect."
|
||||
was_authenticated = False
|
||||
if self.request.user.is_authenticated:
|
||||
# there's already a user logged in, just link them up
|
||||
user = self.request.user
|
||||
was_authenticated = True
|
||||
else:
|
||||
user = self.get_or_create_user(source, access, info)
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
if app('passbook_audit'):
|
||||
pass
|
||||
# TODO: Create audit entry
|
||||
# from passbook.audit.models import something
|
||||
# something.event(user=user,)
|
||||
# Event.create(
|
||||
# user=user,
|
||||
# message=_("Linked user with OAuth source %s" % self.source.name),
|
||||
# request=self.request,
|
||||
# hidden=True,
|
||||
# current=False)
|
||||
if was_authenticated:
|
||||
messages.success(self.request, _("Successfully linked %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
|
||||
'source_slug': self.source.slug
|
||||
}))
|
||||
# User was not authenticated, new user has been created
|
||||
user = authenticate(source=access.source,
|
||||
identifier=access.identifier, request=self.request)
|
||||
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return self.handle_login(user, source, access)
|
||||
|
||||
|
||||
class DisconnectView(LoginRequiredMixin, View):
|
||||
"""Delete connection with source"""
|
||||
|
||||
source = None
|
||||
aas = None
|
||||
|
||||
def dispatch(self, request, source_slug):
|
||||
self.source = get_object_or_404(OAuthSource, slug=source_slug)
|
||||
self.aas = get_object_or_404(UserOAuthSourceConnection,
|
||||
source=self.source, user=request.user)
|
||||
return super().dispatch(request, source_slug)
|
||||
|
||||
def post(self, request, source_slug):
|
||||
"""Delete connection object"""
|
||||
if 'confirmdelete' in request.POST:
|
||||
# User confirmed deletion
|
||||
self.aas.delete()
|
||||
messages.success(request, _('Connection successfully deleted'))
|
||||
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
|
||||
'source_slug': self.source.slug
|
||||
}))
|
||||
return self.get(request, source_slug)
|
||||
|
||||
def get(self, request, source):
|
||||
"""Show delete form"""
|
||||
return render(request, 'generic/delete.html', {
|
||||
'object': self.source,
|
||||
'delete_url': reverse('passbook_oauth_client:oauth-client-disconnect', kwargs={
|
||||
'source_slug': self.source.slug,
|
||||
})
|
||||
})
|
||||
22
passbook/sources/oauth/views/dispatcher.py
Normal file
22
passbook/sources/oauth/views/dispatcher.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Dispatch OAuth views to respective views"""
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
class DispatcherView(View):
|
||||
"""Dispatch OAuth Redirect/Callback views to their proper class based on URL parameters"""
|
||||
|
||||
kind = ''
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find Source by slug and forward request"""
|
||||
slug = kwargs.get('source_slug', None)
|
||||
if not slug:
|
||||
raise Http404
|
||||
source = get_object_or_404(OAuthSource, slug=slug)
|
||||
view = MANAGER.find(source, kind=RequestKind(self.kind))
|
||||
return view.as_view()(*args, **kwargs)
|
||||
21
passbook/sources/oauth/views/user.py
Normal file
21
passbook/sources/oauth/views/user.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""passbook oauth_client user views"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from passbook.sources.oauth.models import (OAuthSource,
|
||||
UserOAuthSourceConnection)
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
"""Show user current connection state"""
|
||||
|
||||
template_name = 'oauth_client/user.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
source = get_object_or_404(OAuthSource, slug=self.kwargs.get('source_slug'))
|
||||
connections = UserOAuthSourceConnection.objects.filter(user=self.request.user,
|
||||
source=source)
|
||||
kwargs['source'] = source
|
||||
kwargs['connections'] = connections
|
||||
return super().get_context_data(**kwargs)
|
||||
Reference in New Issue
Block a user