all: implement black as code formatter

This commit is contained in:
Jens Langhammer
2019-12-31 12:51:16 +01:00
parent 8eb3f0f708
commit 3bd1eadd51
298 changed files with 4825 additions and 3145 deletions

View File

@ -2,4 +2,4 @@
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_sources_oauth')
admin_autoregister("passbook_sources_oauth")

View File

@ -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):

View File

@ -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"""

View 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:

View File

@ -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:

View File

@ -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",
}

View File

@ -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",),
),
]

View File

@ -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")

View File

@ -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",
]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",
),
]

View File

@ -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

View File

@ -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,},
),
},
)

View File

@ -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)

View File

@ -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)