all: implement black as code formatter
This commit is contained in:
@ -2,4 +2,4 @@
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_sources_oauth')
|
||||
admin_autoregister("passbook_sources_oauth")
|
||||
|
||||
@ -11,9 +11,15 @@ class OAuthSourceSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OAuthSource
|
||||
fields = SOURCE_SERIALIZER_FIELDS + ['provider_type', 'request_token_url',
|
||||
'authorization_url', 'access_token_url',
|
||||
'profile_url', 'consumer_key', 'consumer_secret']
|
||||
fields = SOURCE_SERIALIZER_FIELDS + [
|
||||
"provider_type",
|
||||
"request_token_url",
|
||||
"authorization_url",
|
||||
"access_token_url",
|
||||
"profile_url",
|
||||
"consumer_key",
|
||||
"consumer_secret",
|
||||
]
|
||||
|
||||
|
||||
class OAuthSourceViewSet(ModelViewSet):
|
||||
|
||||
@ -7,13 +7,14 @@ 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/'
|
||||
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"""
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db.models import Q
|
||||
|
||||
from passbook.sources.oauth.models import (OAuthSource,
|
||||
UserOAuthSourceConnection)
|
||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
|
||||
class AuthorizedServiceBackend(ModelBackend):
|
||||
@ -18,7 +17,7 @@ class AuthorizedServiceBackend(ModelBackend):
|
||||
try:
|
||||
access = UserOAuthSourceConnection.objects.filter(
|
||||
source_q, identifier=identifier
|
||||
).select_related('user')[0]
|
||||
).select_related("user")[0]
|
||||
except IndexError:
|
||||
return None
|
||||
else:
|
||||
|
||||
@ -20,30 +20,30 @@ class BaseOAuthClient:
|
||||
|
||||
_session = None
|
||||
|
||||
def __init__(self, source, token=''): # nosec
|
||||
def __init__(self, source, token=""): # nosec
|
||||
self.source = source
|
||||
self.token = token
|
||||
self._session = Session()
|
||||
self._session.headers.update({'User-Agent': 'web:passbook:%s' % __version__})
|
||||
self._session.headers.update({"User-Agent": "web:passbook:%s" % __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
|
||||
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 = 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)
|
||||
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
|
||||
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
|
||||
|
||||
def get_redirect_url(self, request, callback, parameters=None):
|
||||
"Build authentication redirect url."
|
||||
@ -52,11 +52,11 @@ class BaseOAuthClient:
|
||||
args.update(additional)
|
||||
params = urlencode(args)
|
||||
LOGGER.info("Redirect args: %s", args)
|
||||
return '{0}?{1}'.format(self.source.authorization_url, params)
|
||||
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
|
||||
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request."
|
||||
@ -67,7 +67,7 @@ class BaseOAuthClient:
|
||||
"""
|
||||
Return Session Key
|
||||
"""
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
|
||||
|
||||
|
||||
class OAuthClient(BaseOAuthClient):
|
||||
@ -76,17 +76,22 @@ class OAuthClient(BaseOAuthClient):
|
||||
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)
|
||||
verifier = request.GET.get("oauth_verifier", None)
|
||||
if raw_token is not None and verifier is not None:
|
||||
data = {'oauth_verifier': verifier}
|
||||
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 = 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)
|
||||
LOGGER.warning("Unable to fetch access token: %s", exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
@ -97,10 +102,11 @@ class OAuthClient(BaseOAuthClient):
|
||||
callback = force_text(request.build_absolute_uri(callback))
|
||||
try:
|
||||
response = self.request(
|
||||
'post', self.source.request_token_url, oauth_callback=callback)
|
||||
"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)
|
||||
LOGGER.warning("Unable to fetch request token: %s", exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
@ -113,8 +119,8 @@ class OAuthClient(BaseOAuthClient):
|
||||
if token is not None and secret is not None:
|
||||
request.session[self.session_key] = raw_token
|
||||
return {
|
||||
'oauth_token': token,
|
||||
'oauth_callback': callback,
|
||||
"oauth_token": token,
|
||||
"oauth_callback": callback,
|
||||
}
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
@ -122,16 +128,16 @@ class OAuthClient(BaseOAuthClient):
|
||||
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]
|
||||
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)
|
||||
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)
|
||||
callback = kwargs.pop("oauth_callback", None)
|
||||
verifier = kwargs.get("data", {}).pop("oauth_verifier", None)
|
||||
oauth = OAuth1(
|
||||
resource_owner_key=token,
|
||||
resource_owner_secret=secret,
|
||||
@ -140,12 +146,12 @@ class OAuthClient(BaseOAuthClient):
|
||||
verifier=verifier,
|
||||
callback_uri=callback,
|
||||
)
|
||||
kwargs['auth'] = oauth
|
||||
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)
|
||||
return "oauth-client-{0}-request-token".format(self.source.name)
|
||||
|
||||
|
||||
class OAuth2Client(BaseOAuthClient):
|
||||
@ -155,40 +161,41 @@ class OAuth2Client(BaseOAuthClient):
|
||||
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)
|
||||
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.')
|
||||
LOGGER.warning("No state parameter returned by the source.")
|
||||
else:
|
||||
LOGGER.warning('No state stored in the sesssion.')
|
||||
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.')
|
||||
LOGGER.warning("Application state check failed.")
|
||||
return None
|
||||
if 'code' in request.GET:
|
||||
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',
|
||||
"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')
|
||||
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 = 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)
|
||||
LOGGER.warning("Unable to fetch access token: %s", exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
@ -202,13 +209,13 @@ class OAuth2Client(BaseOAuthClient):
|
||||
"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',
|
||||
"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
|
||||
args["state"] = state
|
||||
request.session[self.session_key] = state
|
||||
return args
|
||||
|
||||
@ -220,27 +227,27 @@ class OAuth2Client(BaseOAuthClient):
|
||||
try:
|
||||
token_data = json.loads(raw_token)
|
||||
except ValueError:
|
||||
token = parse_qs(raw_token).get('access_token', [None])[0]
|
||||
token = parse_qs(raw_token).get("access_token", [None])[0]
|
||||
else:
|
||||
token = token_data.get('access_token', None)
|
||||
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)
|
||||
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
|
||||
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)
|
||||
return "oauth-client-{0}-request-state".format(self.source.name)
|
||||
|
||||
|
||||
def get_client(source, token=''): # nosec
|
||||
def get_client(source, token=""): # nosec
|
||||
"Return the API client for the given source."
|
||||
cls = OAuth2Client
|
||||
if source.request_token_url:
|
||||
|
||||
@ -14,29 +14,35 @@ class OAuthSourceForm(forms.ModelForm):
|
||||
|
||||
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():
|
||||
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'
|
||||
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']
|
||||
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),
|
||||
"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'),
|
||||
"request_token_url": _("Request Token URL"),
|
||||
"authorization_url": _("Authorization URL"),
|
||||
"access_token_url": _("Access Token URL"),
|
||||
"profile_url": _("Profile URL"),
|
||||
}
|
||||
|
||||
|
||||
@ -46,11 +52,11 @@ class GitHubOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
@ -60,11 +66,11 @@ class TwitterOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
@ -74,11 +80,11 @@ class FacebookOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
@ -88,11 +94,11 @@ class DiscordOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
@ -102,11 +108,11 @@ class GoogleOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
@ -116,9 +122,9 @@ class AzureADOAuthSourceForm(OAuthSourceForm):
|
||||
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',
|
||||
"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",
|
||||
}
|
||||
|
||||
@ -9,39 +9,59 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
("passbook_core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OAuthSource',
|
||||
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()),
|
||||
(
|
||||
"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',
|
||||
"verbose_name": "Generic OAuth Source",
|
||||
"verbose_name_plural": "Generic OAuth Sources",
|
||||
},
|
||||
bases=('passbook_core.source',),
|
||||
bases=("passbook_core.source",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserOAuthSourceConnection',
|
||||
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)),
|
||||
(
|
||||
"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',
|
||||
"verbose_name": "User OAuth Source Connection",
|
||||
"verbose_name_plural": "User OAuth Source Connections",
|
||||
},
|
||||
bases=('passbook_core.usersourceconnection',),
|
||||
bases=("passbook_core.usersourceconnection",),
|
||||
),
|
||||
]
|
||||
|
||||
@ -19,106 +19,111 @@ class OAuthSource(Source):
|
||||
consumer_key = models.TextField()
|
||||
consumer_secret = models.TextField()
|
||||
|
||||
form = 'passbook.sources.oauth.forms.OAuthSourceForm'
|
||||
form = "passbook.sources.oauth.forms.OAuthSourceForm"
|
||||
|
||||
@property
|
||||
def login_button(self):
|
||||
url = reverse_lazy('passbook_sources_oauth:oauth-client-login',
|
||||
kwargs={'source_slug': self.slug})
|
||||
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})
|
||||
return "Callback URL: <pre>%s</pre>" % reverse_lazy(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
kwargs={"source_slug": self.slug},
|
||||
)
|
||||
|
||||
def user_settings(self) -> UserSettings:
|
||||
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 UserSettings(self.name, icon_class, reverse((view_name), kwargs={
|
||||
'source_slug': self.slug
|
||||
}))
|
||||
if icon_type == "azure ad":
|
||||
icon_type = "windows"
|
||||
icon_class = "fa fa-%s" % icon_type
|
||||
view_name = "passbook_sources_oauth:oauth-client-user"
|
||||
return UserSettings(
|
||||
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')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.GitHubOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('GitHub OAuth Source')
|
||||
verbose_name_plural = _('GitHub OAuth Sources')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.TwitterOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Twitter OAuth Source')
|
||||
verbose_name_plural = _('Twitter OAuth Sources')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.FacebookOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Facebook OAuth Source')
|
||||
verbose_name_plural = _('Facebook OAuth Sources')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.DiscordOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Discord OAuth Source')
|
||||
verbose_name_plural = _('Discord OAuth Sources')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.GoogleOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Google OAuth Source')
|
||||
verbose_name_plural = _('Google OAuth Sources')
|
||||
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'
|
||||
form = "passbook.sources.oauth.forms.AzureADOAuthSourceForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _('Azure AD OAuth Source')
|
||||
verbose_name_plural = _('Azure AD OAuth Sources')
|
||||
verbose_name = _("Azure AD OAuth Source")
|
||||
verbose_name_plural = _("Azure AD OAuth Sources")
|
||||
|
||||
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
@ -134,9 +139,9 @@ class UserOAuthSourceConnection(UserSourceConnection):
|
||||
@property
|
||||
def api_client(self):
|
||||
"""Get API Client"""
|
||||
return get_client(self.source, self.access_token or '')
|
||||
return get_client(self.source, self.access_token or "")
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('User OAuth Source Connection')
|
||||
verbose_name_plural = _('User OAuth Source Connections')
|
||||
verbose_name = _("User OAuth Source Connection")
|
||||
verbose_name_plural = _("User OAuth Source Connections")
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
"""Oauth2 Client Settings"""
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'passbook.sources.oauth.backends.AuthorizedServiceBackend',
|
||||
"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',
|
||||
"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",
|
||||
]
|
||||
|
||||
@ -19,34 +19,31 @@ class AzureADOAuth2Client(OAuth2Client):
|
||||
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)
|
||||
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)
|
||||
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')
|
||||
@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
|
||||
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,
|
||||
"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)
|
||||
|
||||
@ -12,13 +12,13 @@ from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='Discord')
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Discord")
|
||||
class DiscordOAuthRedirect(OAuthRedirect):
|
||||
"""Discord OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email identify',
|
||||
"scope": "email identify",
|
||||
}
|
||||
|
||||
|
||||
@ -30,19 +30,23 @@ class DiscordOAuth2Client(OAuth2Client):
|
||||
try:
|
||||
token = json.loads(raw_token)
|
||||
headers = {
|
||||
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
|
||||
"Authorization": "%s %s" % (token["token_type"], token["access_token"])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
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)
|
||||
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')
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Discord")
|
||||
class DiscordOAuth2Callback(OAuthCallback):
|
||||
"""Discord OAuth2 Callback"""
|
||||
|
||||
@ -50,10 +54,10 @@ class DiscordOAuth2Callback(OAuthCallback):
|
||||
|
||||
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,
|
||||
"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
|
||||
|
||||
@ -5,26 +5,26 @@ 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')
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Facebook")
|
||||
class FacebookOAuthRedirect(OAuthRedirect):
|
||||
"""Facebook OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email',
|
||||
"scope": "email",
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Facebook')
|
||||
@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,
|
||||
"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
|
||||
|
||||
@ -5,16 +5,16 @@ 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')
|
||||
@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,
|
||||
"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
|
||||
|
||||
@ -4,26 +4,26 @@ 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')
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Google")
|
||||
class GoogleOAuthRedirect(OAuthRedirect):
|
||||
"""Google OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email profile',
|
||||
"scope": "email profile",
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='Google')
|
||||
@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,
|
||||
"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
|
||||
|
||||
@ -7,11 +7,12 @@ from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class RequestKind(Enum):
|
||||
"""Enum of OAuth Request types"""
|
||||
|
||||
callback = 'callback'
|
||||
redirect = 'redirect'
|
||||
callback = "callback"
|
||||
redirect = "redirect"
|
||||
|
||||
|
||||
class SourceTypeManager:
|
||||
@ -22,6 +23,7 @@ class SourceTypeManager:
|
||||
|
||||
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] = {}
|
||||
@ -29,6 +31,7 @@ class SourceTypeManager:
|
||||
self.__names.append(name)
|
||||
LOGGER.debug("Registered source", source_class=cls.__name__, kind=kind)
|
||||
return cls
|
||||
|
||||
return inner_wrapper
|
||||
|
||||
def get_name_tuple(self):
|
||||
|
||||
@ -13,14 +13,14 @@ from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='reddit')
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="reddit")
|
||||
class RedditOAuthRedirect(OAuthRedirect):
|
||||
"""Reddit OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'identity',
|
||||
'duration': 'permanent',
|
||||
"scope": "identity",
|
||||
"duration": "permanent",
|
||||
}
|
||||
|
||||
|
||||
@ -29,29 +29,33 @@ class RedditOAuth2Client(OAuth2Client):
|
||||
|
||||
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)
|
||||
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'])
|
||||
"Authorization": "%s %s" % (token["token_type"], token["access_token"])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
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)
|
||||
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')
|
||||
@MANAGER.source(kind=RequestKind.callback, name="reddit")
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
"""Reddit OAuth2 Callback"""
|
||||
|
||||
@ -59,10 +63,10 @@ class RedditOAuth2Callback(OAuthCallback):
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user_data = {
|
||||
'username': info.get('name'),
|
||||
'email': None,
|
||||
'name': info.get('name'),
|
||||
'password': None,
|
||||
"username": info.get("name"),
|
||||
"email": None,
|
||||
"name": info.get("name"),
|
||||
"password": None,
|
||||
}
|
||||
reddit_user = user_get_or_create(**user_data)
|
||||
return reddit_user
|
||||
|
||||
@ -19,35 +19,34 @@ class SupervisrOAuth2Client(OAuth2Client):
|
||||
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)
|
||||
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)
|
||||
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')
|
||||
@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']
|
||||
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,
|
||||
"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
|
||||
|
||||
@ -17,17 +17,18 @@ class TwitterOAuthClient(OAuthClient):
|
||||
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 = 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)
|
||||
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')
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Twitter")
|
||||
class TwitterOAuthCallback(OAuthCallback):
|
||||
"""Twitter OAuth2 Callback"""
|
||||
|
||||
@ -35,10 +36,10 @@ class TwitterOAuthCallback(OAuthCallback):
|
||||
|
||||
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,
|
||||
"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
|
||||
|
||||
@ -6,12 +6,24 @@ 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'),
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
||||
@ -12,6 +12,6 @@ def user_get_or_create(**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']
|
||||
kwargs["username"] = "%s_1" % kwargs["username"]
|
||||
new_user = User.objects.create_user(**kwargs)
|
||||
return new_user
|
||||
|
||||
@ -14,8 +14,7 @@ from structlog import get_logger
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
||||
from passbook.sources.oauth.clients import get_client
|
||||
from passbook.sources.oauth.models import (OAuthSource,
|
||||
UserOAuthSourceConnection)
|
||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -47,23 +46,27 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
|
||||
|
||||
def get_callback_url(self, source):
|
||||
"Return the callback url for this source."
|
||||
return reverse('passbook_sources_oauth:oauth-client-callback',
|
||||
kwargs={'source_slug': source.slug})
|
||||
return reverse(
|
||||
"passbook_sources_oauth: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', '')
|
||||
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)
|
||||
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)
|
||||
return client.get_redirect_url(
|
||||
self.request, callback=callback, parameters=params
|
||||
)
|
||||
|
||||
|
||||
class OAuthCallback(OAuthClientMixin, View):
|
||||
@ -74,45 +77,52 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
|
||||
def get(self, request, *_, **kwargs):
|
||||
"""View Get handler"""
|
||||
slug = kwargs.get('source_slug', '')
|
||||
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)
|
||||
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.")
|
||||
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.")
|
||||
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,
|
||||
"access_token": raw_token,
|
||||
}
|
||||
existing = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source, identifier=identifier)
|
||||
source=self.source, identifier=identifier
|
||||
)
|
||||
|
||||
if existing.exists():
|
||||
connection = existing.first()
|
||||
connection.access_token = raw_token
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(**defaults)
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
|
||||
**defaults
|
||||
)
|
||||
else:
|
||||
connection = UserOAuthSourceConnection(
|
||||
source=self.source,
|
||||
identifier=identifier,
|
||||
access_token=raw_token
|
||||
source=self.source, identifier=identifier, access_token=raw_token
|
||||
)
|
||||
user = authenticate(source=self.source, identifier=identifier, request=request)
|
||||
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)
|
||||
@ -136,10 +146,10 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
# 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'
|
||||
id_key = self.source_id or "id"
|
||||
result = info
|
||||
try:
|
||||
for key in id_key.split('.'):
|
||||
for key in id_key.split("."):
|
||||
result = result[key]
|
||||
return result
|
||||
except KeyError:
|
||||
@ -147,25 +157,30 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
}))
|
||||
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.'))
|
||||
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):
|
||||
@ -180,21 +195,31 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
Event.new(EventAction.CUSTOM, message="Linked OAuth Source",
|
||||
source=source.pk).from_http(self.request)
|
||||
Event.new(
|
||||
EventAction.CUSTOM, message="Linked OAuth Source", source=source.pk
|
||||
).from_http(self.request)
|
||||
if was_authenticated:
|
||||
messages.success(self.request, _("Successfully linked %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return redirect(reverse('passbook_sources_oauth:oauth-client-user', kwargs={
|
||||
'source_slug': self.source.slug
|
||||
}))
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"passbook_sources_oauth: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
|
||||
}))
|
||||
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)
|
||||
|
||||
|
||||
@ -206,27 +231,36 @@ class DisconnectView(LoginRequiredMixin, View):
|
||||
|
||||
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)
|
||||
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:
|
||||
if "confirmdelete" in request.POST:
|
||||
# User confirmed deletion
|
||||
self.aas.delete()
|
||||
messages.success(request, _('Connection successfully deleted'))
|
||||
return redirect(reverse('passbook_sources_oauth:oauth-client-user', kwargs={
|
||||
'source_slug': self.source.slug
|
||||
}))
|
||||
messages.success(request, _("Connection successfully deleted"))
|
||||
return redirect(
|
||||
reverse(
|
||||
"passbook_sources_oauth:oauth-client-user",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
)
|
||||
)
|
||||
return self.get(request, source_slug)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, source_slug):
|
||||
"""Show delete form"""
|
||||
return render(request, 'generic/delete.html', {
|
||||
'object': self.source,
|
||||
'delete_url': reverse('passbook_sources_oauth:oauth-client-disconnect', kwargs={
|
||||
'source_slug': self.source.slug,
|
||||
})
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"generic/delete.html",
|
||||
{
|
||||
"object": self.source,
|
||||
"delete_url": reverse(
|
||||
"passbook_sources_oauth:oauth-client-disconnect",
|
||||
kwargs={"source_slug": self.source.slug,},
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@ -10,11 +10,11 @@ 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 = ''
|
||||
kind = ""
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find Source by slug and forward request"""
|
||||
slug = kwargs.get('source_slug', None)
|
||||
slug = kwargs.get("source_slug", None)
|
||||
if not slug:
|
||||
raise Http404
|
||||
source = get_object_or_404(OAuthSource, slug=slug)
|
||||
|
||||
@ -3,19 +3,19 @@ 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)
|
||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
"""Show user current connection state"""
|
||||
|
||||
template_name = 'oauth_client/user.html'
|
||||
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
|
||||
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