diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94605ee3d..54562bc85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: pip install -U pip pipenv && pipenv install --dev - name: Lint with pylint run: pipenv run pylint passbook - isort: + black: runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v1 @@ -41,8 +41,8 @@ jobs: ${{ runner.os }}-pipenv- - name: Install dependencies run: pip install -U pip pipenv && pipenv install --dev - - name: Lint with isort - run: pipenv run isort -c + - name: Lint with black + run: pipenv run black --check passbook prospector: runs-on: [ubuntu-latest] steps: diff --git a/Pipfile b/Pipfile index d87f95786..6cb9ea2e3 100644 --- a/Pipfile +++ b/Pipfile @@ -51,8 +51,11 @@ bumpversion = "*" colorama = "*" coverage = "*" django-debug-toolbar = "*" -isort = "*" prospector = "*" pylint = "*" pylint-django = "*" unittest-xml-reporting = "*" +black = "*" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index 7ce5fb84e..fa4e0143d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5d1d5f5f9664ce6ffb10e89d3780c9e04d4f8f372129baaf3293e44432a2f16d" + "sha256": "138816efaba5be0b175cfd5b5e6a0b58e5ba551567f0efb441740344da3986d8" }, "pipfile-spec": 6, "requires": { @@ -238,11 +238,11 @@ }, "django-prometheus": { "hashes": [ - "sha256:60f331788f9846891e9ea8d7ccd2928b1042e2e99c8d673f97e2b85f5bc20112", - "sha256:bb2d4f8acd681fa5787df77e7482391017f0090c70473bccd2aa7cad327800ad" + "sha256:f0657d4b887309086b71b55f6aa4a95f967b35fe115128b501f95422c423b12c", + "sha256:f645016ae5270ac2025a70788cd2bd636244a0c5705b323cc086994bf828181e" ], "index": "pypi", - "version": "==1.1.0" + "version": "==2.0.0.dev124" }, "django-recaptcha": { "hashes": [ @@ -685,20 +685,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470", + "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292", + "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282", + "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224", + "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058", + "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f", + "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47", + "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b", + "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f", + "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66", + "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4" ], "index": "pypi", - "version": "==5.2" + "version": "==5.3b1" }, "qrcode": { "hashes": [ @@ -858,6 +858,13 @@ } }, "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, "asgiref": { "hashes": [ "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0", @@ -872,6 +879,13 @@ ], "version": "==2.3.3" }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, "autopep8": { "hashes": [ "sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee" @@ -887,6 +901,14 @@ "index": "pypi", "version": "==1.6.2" }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, "bumpversion": { "hashes": [ "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", @@ -895,6 +917,13 @@ "index": "pypi", "version": "==0.5.3" }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, "colorama": { "hashes": [ "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", @@ -981,7 +1010,6 @@ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], - "index": "pypi", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -1017,6 +1045,13 @@ ], "version": "==0.6.1" }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + ], + "version": "==0.7.0" + }, "pbr": { "hashes": [ "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", @@ -1103,20 +1138,46 @@ }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470", + "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292", + "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282", + "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224", + "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058", + "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f", + "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47", + "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b", + "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f", + "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66", + "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4" ], "index": "pypi", - "version": "==5.2" + "version": "==5.3b1" + }, + "regex": { + "hashes": [ + "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", + "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", + "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", + "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", + "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", + "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", + "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", + "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", + "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", + "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", + "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", + "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", + "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", + "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", + "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", + "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", + "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", + "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", + "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", + "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", + "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656" + ], + "version": "==2019.12.20" }, "requirements-detector": { "hashes": [ @@ -1165,6 +1226,13 @@ ], "version": "==1.31.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, "typed-ast": { "hashes": [ "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", diff --git a/passbook/__init__.py b/passbook/__init__.py index 6f6843753..844b69184 100644 --- a/passbook/__init__.py +++ b/passbook/__init__.py @@ -1,2 +1,2 @@ """passbook""" -__version__ = '0.7.5-beta' +__version__ = "0.7.5-beta" diff --git a/passbook/admin/apps.py b/passbook/admin/apps.py index 3827f0b6d..07843888c 100644 --- a/passbook/admin/apps.py +++ b/passbook/admin/apps.py @@ -5,7 +5,7 @@ from django.apps import AppConfig class PassbookAdminConfig(AppConfig): """passbook admin app config""" - name = 'passbook.admin' - label = 'passbook_admin' - mountpoint = 'administration/' - verbose_name = 'passbook Admin' + name = "passbook.admin" + label = "passbook_admin" + mountpoint = "administration/" + verbose_name = "passbook Admin" diff --git a/passbook/admin/fields.py b/passbook/admin/fields.py index 972559399..34702815a 100644 --- a/passbook/admin/fields.py +++ b/passbook/admin/fields.py @@ -16,7 +16,7 @@ class YAMLField(forms.CharField): """Django's JSON Field converted to YAML""" default_error_messages = { - 'invalid': _("'%(value)s' value must be valid YAML."), + "invalid": _("'%(value)s' value must be valid YAML."), } widget = forms.Textarea @@ -31,9 +31,7 @@ class YAMLField(forms.CharField): converted = yaml.safe_load(value) except yaml.YAMLError: raise forms.ValidationError( - self.error_messages['invalid'], - code='invalid', - params={'value': value}, + self.error_messages["invalid"], code="invalid", params={"value": value}, ) if isinstance(converted, str): return YAMLString(converted) diff --git a/passbook/admin/forms/base.py b/passbook/admin/forms/base.py index 9950a6e67..add0954d0 100644 --- a/passbook/admin/forms/base.py +++ b/passbook/admin/forms/base.py @@ -9,29 +9,32 @@ class TagModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): # Check if we have an instance, load tags otherwise use an empty dict - instance = kwargs.get('instance', None) + instance = kwargs.get("instance", None) tags = instance.tags if instance else {} # Make sure all predefined tags exist in tags, and set default if they don't - predefined_tags = self._meta.model().get_predefined_tags() # pylint: disable=no-member + predefined_tags = ( + self._meta.model().get_predefined_tags() + ) # pylint: disable=no-member for key, value in predefined_tags.items(): if key not in tags: tags[key] = value # Format JSON - kwargs['initial']['tags'] = tags + kwargs["initial"]["tags"] = tags super().__init__(*args, **kwargs) def clean_tags(self): """Make sure all required tags are set""" - if hasattr(self.instance, 'get_required_keys') and hasattr(self.instance, 'tags'): + if hasattr(self.instance, "get_required_keys") and hasattr( + self.instance, "tags" + ): for key in self.instance.get_required_keys(): - if key not in self.cleaned_data.get('tags'): + if key not in self.cleaned_data.get("tags"): raise forms.ValidationError("Tag %s missing." % key) - return self.cleaned_data.get('tags') + return self.cleaned_data.get("tags") + # pylint: disable=too-few-public-methods class TagModelFormMeta: """Base Meta class that uses the YAMLField""" - field_classes = { - 'tags': YAMLField - } + field_classes = {"tags": YAMLField} diff --git a/passbook/admin/forms/source.py b/passbook/admin/forms/source.py index 2df4d5db2..d174e58e5 100644 --- a/passbook/admin/forms/source.py +++ b/passbook/admin/forms/source.py @@ -1,7 +1,7 @@ """passbook core source form fields""" # from django import forms -SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled', 'policies'] -SOURCE_SERIALIZER_FIELDS = ['pk', 'name', 'slug', 'enabled', 'policies'] +SOURCE_FORM_FIELDS = ["name", "slug", "enabled", "policies"] +SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled", "policies"] # class SourceForm(forms.Form) diff --git a/passbook/admin/forms/users.py b/passbook/admin/forms/users.py index 50fcedffa..f81b22a8d 100644 --- a/passbook/admin/forms/users.py +++ b/passbook/admin/forms/users.py @@ -12,10 +12,10 @@ class UserForm(forms.ModelForm): class Meta: model = User - fields = ['username', 'name', 'email', 'is_staff', 'is_active', 'attributes'] + fields = ["username", "name", "email", "is_staff", "is_active", "attributes"] widgets = { - 'name': forms.TextInput, + "name": forms.TextInput, } field_classes = { - 'attributes': YAMLField, + "attributes": YAMLField, } diff --git a/passbook/admin/middleware.py b/passbook/admin/middleware.py index 1c4d1c42a..0be230711 100644 --- a/passbook/admin/middleware.py +++ b/passbook/admin/middleware.py @@ -11,15 +11,16 @@ def impersonate(get_response): # User is superuser and has __impersonate ID set if request.user.is_superuser and "__impersonate" in request.GET: - request.session['impersonate_id'] = request.GET["__impersonate"] + request.session["impersonate_id"] = request.GET["__impersonate"] # user wants to stop impersonation - elif "__unimpersonate" in request.GET and 'impersonate_id' in request.session: - del request.session['impersonate_id'] + elif "__unimpersonate" in request.GET and "impersonate_id" in request.session: + del request.session["impersonate_id"] # Actually impersonate user - if request.user.is_superuser and 'impersonate_id' in request.session: - request.user = User.objects.get(pk=request.session['impersonate_id']) + if request.user.is_superuser and "impersonate_id" in request.session: + request.user = User.objects.get(pk=request.session["impersonate_id"]) response = get_response(request) return response + return middleware diff --git a/passbook/admin/settings.py b/passbook/admin/settings.py index 14f5e9421..09bb5c9f7 100644 --- a/passbook/admin/settings.py +++ b/passbook/admin/settings.py @@ -1,5 +1,5 @@ """passbook admin settings""" MIDDLEWARE = [ - 'passbook.admin.middleware.impersonate', + "passbook.admin.middleware.impersonate", ] diff --git a/passbook/admin/templatetags/admin_reflection.py b/passbook/admin/templatetags/admin_reflection.py index fa01a0099..311b259e6 100644 --- a/passbook/admin/templatetags/admin_reflection.py +++ b/passbook/admin/templatetags/admin_reflection.py @@ -10,10 +10,11 @@ from passbook.lib.utils.template import render_to_string register = template.Library() LOGGER = get_logger() + @register.simple_tag() def get_links(model_instance): """Find all link_ methods on an object instance, run them and return as dict""" - prefix = 'link_' + prefix = "link_" links = {} if not isinstance(model_instance, Model): @@ -21,9 +22,11 @@ def get_links(model_instance): return links try: - for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): + for name, method in inspect.getmembers( + model_instance, predicate=inspect.ismethod + ): if name.startswith(prefix): - human_name = name.replace(prefix, '').replace('_', ' ').capitalize() + human_name = name.replace(prefix, "").replace("_", " ").capitalize() link = method() if link: links[human_name] = link @@ -36,7 +39,7 @@ def get_links(model_instance): @register.simple_tag(takes_context=True) def get_htmls(context, model_instance): """Find all html_ methods on an object instance, run them and return as dict""" - prefix = 'html_' + prefix = "html_" htmls = [] if not isinstance(model_instance, Model): @@ -44,9 +47,11 @@ def get_htmls(context, model_instance): return htmls try: - for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): + for name, method in inspect.getmembers( + model_instance, predicate=inspect.ismethod + ): if name.startswith(prefix): - template, _context = method(context.get('request')) + template, _context = method(context.get("request")) htmls.append(render_to_string(template, _context)) except NotImplementedError: pass diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 3a21d7785..51279ecad 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -1,82 +1,157 @@ """passbook URL Configuration""" from django.urls import path -from passbook.admin.views import (applications, audit, debug, factors, groups, - invitations, overview, policy, - property_mapping, providers, sources, users) +from passbook.admin.views import ( + applications, + audit, + debug, + factors, + groups, + invitations, + overview, + policy, + property_mapping, + providers, + sources, + users, +) urlpatterns = [ - path('', overview.AdministrationOverviewView.as_view(), name='overview'), + path("", overview.AdministrationOverviewView.as_view(), name="overview"), # Applications - path('applications/', applications.ApplicationListView.as_view(), - name='applications'), - path('applications/create/', applications.ApplicationCreateView.as_view(), - name='application-create'), - path('applications//update/', - applications.ApplicationUpdateView.as_view(), name='application-update'), - path('applications//delete/', - applications.ApplicationDeleteView.as_view(), name='application-delete'), + path( + "applications/", applications.ApplicationListView.as_view(), name="applications" + ), + path( + "applications/create/", + applications.ApplicationCreateView.as_view(), + name="application-create", + ), + path( + "applications//update/", + applications.ApplicationUpdateView.as_view(), + name="application-update", + ), + path( + "applications//delete/", + applications.ApplicationDeleteView.as_view(), + name="application-delete", + ), # Sources - path('sources/', sources.SourceListView.as_view(), name='sources'), - path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'), - path('sources//update/', sources.SourceUpdateView.as_view(), name='source-update'), - path('sources//delete/', sources.SourceDeleteView.as_view(), name='source-delete'), + path("sources/", sources.SourceListView.as_view(), name="sources"), + path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"), + path( + "sources//update/", + sources.SourceUpdateView.as_view(), + name="source-update", + ), + path( + "sources//delete/", + sources.SourceDeleteView.as_view(), + name="source-delete", + ), # Policies - path('policies/', policy.PolicyListView.as_view(), name='policies'), - path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'), - path('policies//update/', policy.PolicyUpdateView.as_view(), name='policy-update'), - path('policies//delete/', policy.PolicyDeleteView.as_view(), name='policy-delete'), - path('policies//test/', policy.PolicyTestView.as_view(), name='policy-test'), + path("policies/", policy.PolicyListView.as_view(), name="policies"), + path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"), + path( + "policies//update/", + policy.PolicyUpdateView.as_view(), + name="policy-update", + ), + path( + "policies//delete/", + policy.PolicyDeleteView.as_view(), + name="policy-delete", + ), + path( + "policies//test/", policy.PolicyTestView.as_view(), name="policy-test" + ), # Providers - path('providers/', providers.ProviderListView.as_view(), name='providers'), - path('providers/create/', - providers.ProviderCreateView.as_view(), name='provider-create'), - path('providers//update/', - providers.ProviderUpdateView.as_view(), name='provider-update'), - path('providers//delete/', - providers.ProviderDeleteView.as_view(), name='provider-delete'), + path("providers/", providers.ProviderListView.as_view(), name="providers"), + path( + "providers/create/", + providers.ProviderCreateView.as_view(), + name="provider-create", + ), + path( + "providers//update/", + providers.ProviderUpdateView.as_view(), + name="provider-update", + ), + path( + "providers//delete/", + providers.ProviderDeleteView.as_view(), + name="provider-delete", + ), # Factors - path('factors/', factors.FactorListView.as_view(), name='factors'), - path('factors/create/', - factors.FactorCreateView.as_view(), name='factor-create'), - path('factors//update/', - factors.FactorUpdateView.as_view(), name='factor-update'), - path('factors//delete/', - factors.FactorDeleteView.as_view(), name='factor-delete'), + path("factors/", factors.FactorListView.as_view(), name="factors"), + path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"), + path( + "factors//update/", + factors.FactorUpdateView.as_view(), + name="factor-update", + ), + path( + "factors//delete/", + factors.FactorDeleteView.as_view(), + name="factor-delete", + ), # Factors - path('property-mappings/', property_mapping.PropertyMappingListView.as_view(), - name='property-mappings'), - path('property-mappings/create/', - property_mapping.PropertyMappingCreateView.as_view(), name='property-mapping-create'), - path('property-mappings//update/', - property_mapping.PropertyMappingUpdateView.as_view(), name='property-mapping-update'), - path('property-mappings//delete/', - property_mapping.PropertyMappingDeleteView.as_view(), name='property-mapping-delete'), + path( + "property-mappings/", + property_mapping.PropertyMappingListView.as_view(), + name="property-mappings", + ), + path( + "property-mappings/create/", + property_mapping.PropertyMappingCreateView.as_view(), + name="property-mapping-create", + ), + path( + "property-mappings//update/", + property_mapping.PropertyMappingUpdateView.as_view(), + name="property-mapping-update", + ), + path( + "property-mappings//delete/", + property_mapping.PropertyMappingDeleteView.as_view(), + name="property-mapping-delete", + ), # Invitations - path('invitations/', invitations.InvitationListView.as_view(), name='invitations'), - path('invitations/create/', - invitations.InvitationCreateView.as_view(), name='invitation-create'), - path('invitations//delete/', - invitations.InvitationDeleteView.as_view(), name='invitation-delete'), + path("invitations/", invitations.InvitationListView.as_view(), name="invitations"), + path( + "invitations/create/", + invitations.InvitationCreateView.as_view(), + name="invitation-create", + ), + path( + "invitations//delete/", + invitations.InvitationDeleteView.as_view(), + name="invitation-delete", + ), # Users - path('users/', users.UserListView.as_view(), - name='users'), - path('users/create/', users.UserCreateView.as_view(), name='user-create'), - path('users//update/', - users.UserUpdateView.as_view(), name='user-update'), - path('users//delete/', - users.UserDeleteView.as_view(), name='user-delete'), - path('users//reset/', - users.UserPasswordResetView.as_view(), name='user-password-reset'), + path("users/", users.UserListView.as_view(), name="users"), + path("users/create/", users.UserCreateView.as_view(), name="user-create"), + path("users//update/", users.UserUpdateView.as_view(), name="user-update"), + path("users//delete/", users.UserDeleteView.as_view(), name="user-delete"), + path( + "users//reset/", + users.UserPasswordResetView.as_view(), + name="user-password-reset", + ), # Groups - path('group/', groups.GroupListView.as_view(), name='group'), - path('group/create/', groups.GroupCreateView.as_view(), name='group-create'), - path('group//update/', groups.GroupUpdateView.as_view(), name='group-update'), - path('group//delete/', groups.GroupDeleteView.as_view(), name='group-delete'), + path("group/", groups.GroupListView.as_view(), name="group"), + path("group/create/", groups.GroupCreateView.as_view(), name="group-create"), + path( + "group//update/", groups.GroupUpdateView.as_view(), name="group-update" + ), + path( + "group//delete/", groups.GroupDeleteView.as_view(), name="group-delete" + ), # Audit Log - path('audit/', audit.EventListView.as_view(), name='audit-log'), + path("audit/", audit.EventListView.as_view(), name="audit-log"), # Groups - path('groups/', groups.GroupListView.as_view(), name='groups'), + path("groups/", groups.GroupListView.as_view(), name="groups"), # Debug - path('debug/request/', debug.DebugRequestView.as_view(), name='debug-request'), + path("debug/request/", debug.DebugRequestView.as_view(), name="debug-request"), ] diff --git a/passbook/admin/views/applications.py b/passbook/admin/views/applications.py index ba3fb40a3..357e1ab2c 100644 --- a/passbook/admin/views/applications.py +++ b/passbook/admin/views/applications.py @@ -1,8 +1,9 @@ """passbook Application administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy from django.utils.translation import ugettext as _ @@ -18,55 +19,61 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all applications""" model = Application - permission_required = 'passbook_core.view_application' - ordering = 'name' + permission_required = "passbook_core.view_application" + ordering = "name" paginate_by = 40 - template_name = 'administration/application/list.html' + template_name = "administration/application/list.html" def get_queryset(self): return super().get_queryset().select_subclasses() -class ApplicationCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class ApplicationCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Application""" model = Application form_class = ApplicationForm - permission_required = 'passbook_core.add_application' + permission_required = "passbook_core.add_application" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:applications') - success_message = _('Successfully created Application') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:applications") + success_message = _("Successfully created Application") def get_context_data(self, **kwargs): - kwargs['type'] = 'Application' + kwargs["type"] = "Application" return super().get_context_data(**kwargs) -class ApplicationUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class ApplicationUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update application""" model = Application form_class = ApplicationForm - permission_required = 'passbook_core.change_application' + permission_required = "passbook_core.change_application" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:applications') - success_message = _('Successfully updated Application') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:applications") + success_message = _("Successfully updated Application") -class ApplicationDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class ApplicationDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete application""" model = Application - permission_required = 'passbook_core.delete_application' + permission_required = "passbook_core.delete_application" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:applications') - success_message = _('Successfully deleted Application') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:applications") + success_message = _("Successfully deleted Application") def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/audit.py b/passbook/admin/views/audit.py index 95498917e..b57198378 100644 --- a/passbook/admin/views/audit.py +++ b/passbook/admin/views/audit.py @@ -9,10 +9,10 @@ class EventListView(PermissionListMixin, ListView): """Show list of all invitations""" model = Event - template_name = 'administration/audit/list.html' - permission_required = 'passbook_audit.view_event' - ordering = '-created' + template_name = "administration/audit/list.html" + permission_required = "passbook_audit.view_event" + ordering = "-created" paginate_by = 10 def get_queryset(self): - return Event.objects.all().order_by('-created') + return Event.objects.all().order_by("-created") diff --git a/passbook/admin/views/debug.py b/passbook/admin/views/debug.py index 4971c15ae..d6dbc2f6c 100644 --- a/passbook/admin/views/debug.py +++ b/passbook/admin/views/debug.py @@ -6,10 +6,10 @@ from django.views.generic import TemplateView class DebugRequestView(LoginRequiredMixin, TemplateView): """Show debug info about request""" - template_name = 'administration/debug/request.html' + template_name = "administration/debug/request.html" def get_context_data(self, **kwargs): - kwargs['request_dict'] = {} + kwargs["request_dict"] = {} for key in dir(self.request): - kwargs['request_dict'][key] = getattr(self.request, key) + kwargs["request_dict"][key] = getattr(self.request, key) return super().get_context_data(**kwargs) diff --git a/passbook/admin/views/factors.py b/passbook/admin/views/factors.py index ad26a4ab7..628c6a61f 100644 --- a/passbook/admin/views/factors.py +++ b/passbook/admin/views/factors.py @@ -1,8 +1,9 @@ """passbook Factor administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.urls import reverse_lazy @@ -18,62 +19,69 @@ from passbook.lib.views import CreateAssignPermView def all_subclasses(cls): """Recursively return all subclassess of cls""" return set(cls.__subclasses__()).union( - [s for c in cls.__subclasses__() for s in all_subclasses(c)]) + [s for c in cls.__subclasses__() for s in all_subclasses(c)] + ) class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all factors""" model = Factor - template_name = 'administration/factor/list.html' - permission_required = 'passbook_core.view_factor' - ordering = 'order' + template_name = "administration/factor/list.html" + permission_required = "passbook_core.view_factor" + ordering = "order" paginate_by = 40 def get_context_data(self, **kwargs): - kwargs['types'] = { - x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)} + kwargs["types"] = { + x.__name__: x._meta.verbose_name for x in all_subclasses(Factor) + } return super().get_context_data(**kwargs) def get_queryset(self): return super().get_queryset().select_subclasses() -class FactorCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class FactorCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Factor""" model = Factor - template_name = 'generic/create.html' - permission_required = 'passbook_core.add_factor' + template_name = "generic/create.html" + permission_required = "passbook_core.add_factor" - success_url = reverse_lazy('passbook_admin:factors') - success_message = _('Successfully created Factor') + success_url = reverse_lazy("passbook_admin:factors") + success_message = _("Successfully created Factor") def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - factor_type = self.request.GET.get('type') + factor_type = self.request.GET.get("type") model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) - kwargs['type'] = model._meta.verbose_name + kwargs["type"] = model._meta.verbose_name return kwargs def get_form_class(self): - factor_type = self.request.GET.get('type') + factor_type = self.request.GET.get("type") model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) if not model: raise Http404 return path_to_class(model.form) -class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class FactorUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update factor""" model = Factor - permission_required = 'passbook_core.update_application' - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:factors') - success_message = _('Successfully updated Factor') + permission_required = "passbook_core.update_application" + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:factors") + success_message = _("Successfully updated Factor") def get_form_class(self): form_class_path = self.get_object().form @@ -81,21 +89,26 @@ class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin, return form_class def get_object(self, queryset=None): - return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) -class FactorDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class FactorDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete factor""" model = Factor - template_name = 'generic/delete.html' - permission_required = 'passbook_core.delete_factor' - success_url = reverse_lazy('passbook_admin:factors') - success_message = _('Successfully deleted Factor') + template_name = "generic/delete.html" + permission_required = "passbook_core.delete_factor" + success_url = reverse_lazy("passbook_admin:factors") + success_message = _("Successfully deleted Factor") def get_object(self, queryset=None): - return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/groups.py b/passbook/admin/views/groups.py index dbe9e2dff..4a5c91357 100644 --- a/passbook/admin/views/groups.py +++ b/passbook/admin/views/groups.py @@ -1,8 +1,9 @@ """passbook Group administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy from django.utils.translation import ugettext as _ @@ -18,40 +19,45 @@ class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all groups""" model = Group - permission_required = 'passbook_core.view_group' - ordering = 'name' + permission_required = "passbook_core.view_group" + ordering = "name" paginate_by = 40 - template_name = 'administration/group/list.html' + template_name = "administration/group/list.html" -class GroupCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class GroupCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Group""" model = Group form_class = GroupForm - permission_required = 'passbook_core.add_group' + permission_required = "passbook_core.add_group" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:groups') - success_message = _('Successfully created Group') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:groups") + success_message = _("Successfully created Group") def get_context_data(self, **kwargs): - kwargs['type'] = 'Group' + kwargs["type"] = "Group" return super().get_context_data(**kwargs) -class GroupUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class GroupUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update group""" model = Group form_class = GroupForm - permission_required = 'passbook_core.change_group' + permission_required = "passbook_core.change_group" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:groups') - success_message = _('Successfully updated Group') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:groups") + success_message = _("Successfully updated Group") class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView): @@ -59,9 +65,9 @@ class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView): model = Group - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:groups') - success_message = _('Successfully deleted Group') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:groups") + success_message = _("Successfully deleted Group") def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/invitations.py b/passbook/admin/views/invitations.py index dc3dd372f..87ad0f3e2 100644 --- a/passbook/admin/views/invitations.py +++ b/passbook/admin/views/invitations.py @@ -1,8 +1,9 @@ """passbook Invitation administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponseRedirect from django.urls import reverse_lazy @@ -20,47 +21,49 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all invitations""" model = Invitation - permission_required = 'passbook_core.view_invitation' - template_name = 'administration/invitation/list.html' + permission_required = "passbook_core.view_invitation" + template_name = "administration/invitation/list.html" -class InvitationCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class InvitationCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Invitation""" model = Invitation form_class = InvitationForm - permission_required = 'passbook_core.add_invitation' + permission_required = "passbook_core.add_invitation" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:invitations') - success_message = _('Successfully created Invitation') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:invitations") + success_message = _("Successfully created Invitation") def get_context_data(self, **kwargs): - kwargs['type'] = 'Invitation' + kwargs["type"] = "Invitation" return super().get_context_data(**kwargs) def form_valid(self, form): obj = form.save(commit=False) obj.created_by = self.request.user obj.save() - invitation_created.send( - sender=self, - request=self.request, - invitation=obj) + invitation_created.send(sender=self, request=self.request, invitation=obj) return HttpResponseRedirect(self.success_url) -class InvitationDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class InvitationDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete invitation""" model = Invitation - permission_required = 'passbook_core.delete_invitation' + permission_required = "passbook_core.delete_invitation" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:invitations') - success_message = _('Successfully deleted Invitation') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:invitations") + success_message = _("Successfully deleted Invitation") def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py index 679792828..f4e8737b6 100644 --- a/passbook/admin/views/overview.py +++ b/passbook/admin/views/overview.py @@ -5,34 +5,45 @@ from django.views.generic import TemplateView from passbook import __version__ from passbook.admin.mixins import AdminRequiredMixin -from passbook.core.models import (Application, Factor, Invitation, Policy, - Provider, Source, User) +from passbook.core.models import ( + Application, + Factor, + Invitation, + Policy, + Provider, + Source, + User, +) from passbook.root.celery import CELERY_APP class AdministrationOverviewView(AdminRequiredMixin, TemplateView): """Overview View""" - template_name = 'administration/overview.html' + template_name = "administration/overview.html" def post(self, *args, **kwargs): """Handle post (clear cache from modal)""" - if 'clear' in self.request.POST: + if "clear" in self.request.POST: cache.clear() - return redirect(reverse('passbook_core:auth-login')) + return redirect(reverse("passbook_core:auth-login")) return self.get(*args, **kwargs) def get_context_data(self, **kwargs): - kwargs['application_count'] = len(Application.objects.all()) - kwargs['policy_count'] = len(Policy.objects.all()) - kwargs['user_count'] = len(User.objects.all()) - kwargs['provider_count'] = len(Provider.objects.all()) - kwargs['source_count'] = len(Source.objects.all()) - kwargs['factor_count'] = len(Factor.objects.all()) - kwargs['invitation_count'] = len(Invitation.objects.all()) - kwargs['version'] = __version__ - kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5)) - kwargs['providers_without_application'] = Provider.objects.filter(application=None) - kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True)) - kwargs['cached_policies'] = len(cache.keys('policy_*')) + kwargs["application_count"] = len(Application.objects.all()) + kwargs["policy_count"] = len(Policy.objects.all()) + kwargs["user_count"] = len(User.objects.all()) + kwargs["provider_count"] = len(Provider.objects.all()) + kwargs["source_count"] = len(Source.objects.all()) + kwargs["factor_count"] = len(Factor.objects.all()) + kwargs["invitation_count"] = len(Invitation.objects.all()) + kwargs["version"] = __version__ + kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5)) + kwargs["providers_without_application"] = Provider.objects.filter( + application=None + ) + kwargs["policies_without_attachment"] = len( + Policy.objects.filter(policymodel__isnull=True) + ) + kwargs["cached_policies"] = len(cache.keys("policy_*")) return super().get_context_data(**kwargs) diff --git a/passbook/admin/views/policy.py b/passbook/admin/views/policy.py index 8933e58a2..6c9c2bbc1 100644 --- a/passbook/admin/views/policy.py +++ b/passbook/admin/views/policy.py @@ -1,8 +1,9 @@ """passbook Policy administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.urls import reverse_lazy @@ -22,49 +23,54 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all policies""" model = Policy - permission_required = 'passbook_core.view_policy' + permission_required = "passbook_core.view_policy" - template_name = 'administration/policy/list.html' + template_name = "administration/policy/list.html" def get_context_data(self, **kwargs): - kwargs['types'] = { - x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()} + kwargs["types"] = { + x.__name__: x._meta.verbose_name for x in Policy.__subclasses__() + } return super().get_context_data(**kwargs) def get_queryset(self): - return super().get_queryset().order_by('order').select_subclasses() + return super().get_queryset().order_by("order").select_subclasses() -class PolicyCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class PolicyCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Policy""" model = Policy - permission_required = 'passbook_core.add_policy' + permission_required = "passbook_core.add_policy" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:policies') - success_message = _('Successfully created Policy') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully created Policy") def get_form_class(self): - policy_type = self.request.GET.get('type') - model = next(x for x in Policy.__subclasses__() - if x.__name__ == policy_type) + policy_type = self.request.GET.get("type") + model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_type) if not model: raise Http404 return path_to_class(model.form) -class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class PolicyUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update policy""" model = Policy - permission_required = 'passbook_core.change_policy' + permission_required = "passbook_core.change_policy" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:policies') - success_message = _('Successfully updated Policy') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully updated Policy") def get_form_class(self): form_class_path = self.get_object().form @@ -72,22 +78,27 @@ class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin, return form_class def get_object(self, queryset=None): - return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) -class PolicyDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class PolicyDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete policy""" model = Policy - permission_required = 'passbook_core.delete_policy' + permission_required = "passbook_core.delete_policy" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:policies') - success_message = _('Successfully deleted Policy') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:policies") + success_message = _("Successfully deleted Policy") def get_object(self, queryset=None): - return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) @@ -99,15 +110,17 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo model = Policy form_class = PolicyTestForm - permission_required = 'passbook_core.view_policy' - template_name = 'administration/policy/test.html' + permission_required = "passbook_core.view_policy" + template_name = "administration/policy/test.html" object = None def get_object(self, queryset=None): - return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) def get_context_data(self, **kwargs): - kwargs['policy'] = self.get_object() + kwargs["policy"] = self.get_object() return super().get_context_data(**kwargs) def post(self, *args, **kwargs): @@ -116,13 +129,13 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo def form_valid(self, form): policy = self.get_object() - user = form.cleaned_data.get('user') + user = form.cleaned_data.get("user") policy_engine = PolicyEngine([policy], user, self.request) policy_engine.use_cache = False policy_engine.build() result = policy_engine.passing if result: - messages.success(self.request, _('User successfully passed policy.')) + messages.success(self.request, _("User successfully passed policy.")) else: messages.error(self.request, _("User didn't pass policy.")) return self.render_to_response(self.get_context_data(form=form, result=result)) diff --git a/passbook/admin/views/property_mapping.py b/passbook/admin/views/property_mapping.py index bbfdf2bc3..d87ca4d42 100644 --- a/passbook/admin/views/property_mapping.py +++ b/passbook/admin/views/property_mapping.py @@ -1,8 +1,9 @@ """passbook PropertyMapping administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.urls import reverse_lazy @@ -18,65 +19,78 @@ from passbook.lib.views import CreateAssignPermView def all_subclasses(cls): """Recursively return all subclassess of cls""" return set(cls.__subclasses__()).union( - [s for c in cls.__subclasses__() for s in all_subclasses(c)]) + [s for c in cls.__subclasses__() for s in all_subclasses(c)] + ) class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all property_mappings""" model = PropertyMapping - permission_required = 'passbook_core.view_propertymapping' - template_name = 'administration/property_mapping/list.html' - ordering = 'name' + permission_required = "passbook_core.view_propertymapping" + template_name = "administration/property_mapping/list.html" + ordering = "name" paginate_by = 40 def get_context_data(self, **kwargs): - kwargs['types'] = { - x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)} + kwargs["types"] = { + x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping) + } return super().get_context_data(**kwargs) def get_queryset(self): return super().get_queryset().select_subclasses() -class PropertyMappingCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class PropertyMappingCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new PropertyMapping""" model = PropertyMapping - permission_required = 'passbook_core.add_propertymapping' + permission_required = "passbook_core.add_propertymapping" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:property-mappings') - success_message = _('Successfully created Property Mapping') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:property-mappings") + success_message = _("Successfully created Property Mapping") def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - property_mapping_type = self.request.GET.get('type') - model = next(x for x in all_subclasses(PropertyMapping) - if x.__name__ == property_mapping_type) - kwargs['type'] = model._meta.verbose_name + property_mapping_type = self.request.GET.get("type") + model = next( + x + for x in all_subclasses(PropertyMapping) + if x.__name__ == property_mapping_type + ) + kwargs["type"] = model._meta.verbose_name return kwargs def get_form_class(self): - property_mapping_type = self.request.GET.get('type') - model = next(x for x in all_subclasses(PropertyMapping) - if x.__name__ == property_mapping_type) + property_mapping_type = self.request.GET.get("type") + model = next( + x + for x in all_subclasses(PropertyMapping) + if x.__name__ == property_mapping_type + ) if not model: raise Http404 return path_to_class(model.form) -class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class PropertyMappingUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update property_mapping""" model = PropertyMapping - permission_required = 'passbook_core.change_propertymapping' + permission_required = "passbook_core.change_propertymapping" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:property-mappings') - success_message = _('Successfully updated Property Mapping') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:property-mappings") + success_message = _("Successfully updated Property Mapping") def get_form_class(self): form_class_path = self.get_object().form @@ -84,22 +98,31 @@ class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin, return form_class def get_object(self, queryset=None): - return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + PropertyMapping.objects.filter(pk=self.kwargs.get("pk")) + .select_subclasses() + .first() + ) -class PropertyMappingDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class PropertyMappingDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete property_mapping""" model = PropertyMapping - permission_required = 'passbook_core.delete_propertymapping' + permission_required = "passbook_core.delete_propertymapping" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:property-mappings') - success_message = _('Successfully deleted Property Mapping') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:property-mappings") + success_message = _("Successfully deleted Property Mapping") def get_object(self, queryset=None): - return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + PropertyMapping.objects.filter(pk=self.kwargs.get("pk")) + .select_subclasses() + .first() + ) def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/providers.py b/passbook/admin/views/providers.py index 0f226a787..a7cf76c65 100644 --- a/passbook/admin/views/providers.py +++ b/passbook/admin/views/providers.py @@ -1,8 +1,9 @@ """passbook Provider administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.urls import reverse_lazy @@ -19,48 +20,55 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all providers""" model = Provider - permission_required = 'passbook_core.add_provider' - template_name = 'administration/provider/list.html' + permission_required = "passbook_core.add_provider" + template_name = "administration/provider/list.html" def get_context_data(self, **kwargs): - kwargs['types'] = { - x.__name__: x._meta.verbose_name for x in Provider.__subclasses__()} + kwargs["types"] = { + x.__name__: x._meta.verbose_name for x in Provider.__subclasses__() + } return super().get_context_data(**kwargs) def get_queryset(self): return super().get_queryset().select_subclasses() -class ProviderCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class ProviderCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Provider""" model = Provider - permission_required = 'passbook_core.add_provider' + permission_required = "passbook_core.add_provider" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:providers') - success_message = _('Successfully created Provider') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:providers") + success_message = _("Successfully created Provider") def get_form_class(self): - provider_type = self.request.GET.get('type') - model = next(x for x in Provider.__subclasses__() - if x.__name__ == provider_type) + provider_type = self.request.GET.get("type") + model = next( + x for x in Provider.__subclasses__() if x.__name__ == provider_type + ) if not model: raise Http404 return path_to_class(model.form) -class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class ProviderUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update provider""" model = Provider - permission_required = 'passbook_core.change_provider' + permission_required = "passbook_core.change_provider" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:providers') - success_message = _('Successfully updated Provider') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:providers") + success_message = _("Successfully updated Provider") def get_form_class(self): form_class_path = self.get_object().form @@ -68,22 +76,31 @@ class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin, return form_class def get_object(self, queryset=None): - return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Provider.objects.filter(pk=self.kwargs.get("pk")) + .select_subclasses() + .first() + ) -class ProviderDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class ProviderDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete provider""" model = Provider - permission_required = 'passbook_core.delete_provider' + permission_required = "passbook_core.delete_provider" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:providers') - success_message = _('Successfully deleted Provider') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:providers") + success_message = _("Successfully deleted Provider") def get_object(self, queryset=None): - return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Provider.objects.filter(pk=self.kwargs.get("pk")) + .select_subclasses() + .first() + ) def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/sources.py b/passbook/admin/views/sources.py index 3e2c19b8a..68c1bd422 100644 --- a/passbook/admin/views/sources.py +++ b/passbook/admin/views/sources.py @@ -1,8 +1,9 @@ """passbook Source administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.urls import reverse_lazy @@ -18,55 +19,63 @@ from passbook.lib.views import CreateAssignPermView def all_subclasses(cls): """Recursively return all subclassess of cls""" return set(cls.__subclasses__()).union( - [s for c in cls.__subclasses__() for s in all_subclasses(c)]) + [s for c in cls.__subclasses__() for s in all_subclasses(c)] + ) + class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all sources""" model = Source - permission_required = 'passbook_core.view_source' - ordering = 'name' + permission_required = "passbook_core.view_source" + ordering = "name" paginate_by = 40 - template_name = 'administration/source/list.html' + template_name = "administration/source/list.html" def get_context_data(self, **kwargs): - kwargs['types'] = { - x.__name__: x._meta.verbose_name for x in all_subclasses(Source)} + kwargs["types"] = { + x.__name__: x._meta.verbose_name for x in all_subclasses(Source) + } return super().get_context_data(**kwargs) def get_queryset(self): return super().get_queryset().select_subclasses() -class SourceCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class SourceCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create new Source""" model = Source - permission_required = 'passbook_core.add_source' + permission_required = "passbook_core.add_source" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:sources') - success_message = _('Successfully created Source') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:sources") + success_message = _("Successfully created Source") def get_form_class(self): - source_type = self.request.GET.get('type') + source_type = self.request.GET.get("type") model = next(x for x in all_subclasses(Source) if x.__name__ == source_type) if not model: raise Http404 return path_to_class(model.form) -class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class SourceUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update source""" model = Source - permission_required = 'passbook_core.change_source' + permission_required = "passbook_core.change_source" - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:sources') - success_message = _('Successfully updated Source') + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:sources") + success_message = _("Successfully updated Source") def get_form_class(self): form_class_path = self.get_object().form @@ -74,22 +83,27 @@ class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin, return form_class def get_object(self, queryset=None): - return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) -class SourceDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class SourceDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete source""" model = Source - permission_required = 'passbook_core.delete_source' + permission_required = "passbook_core.delete_source" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:sources') - success_message = _('Successfully deleted Source') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:sources") + success_message = _("Successfully deleted Source") def get_object(self, queryset=None): - return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() + return ( + Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + ) def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) diff --git a/passbook/admin/views/users.py b/passbook/admin/views/users.py index 535d4d264..33fa17f7b 100644 --- a/passbook/admin/views/users.py +++ b/passbook/admin/views/users.py @@ -1,8 +1,9 @@ """passbook User administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import \ - PermissionRequiredMixin as DjangoPermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin as DjangoPermissionRequiredMixin, +) from django.contrib.messages.views import SuccessMessageMixin from django.shortcuts import redirect from django.urls import reverse, reverse_lazy @@ -19,50 +20,56 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView): """Show list of all users""" model = User - permission_required = 'passbook_core.view_user' - ordering = 'username' + permission_required = "passbook_core.view_user" + ordering = "username" paginate_by = 40 - template_name = 'administration/user/list.html' + template_name = "administration/user/list.html" -class UserCreateView(SuccessMessageMixin, LoginRequiredMixin, - DjangoPermissionRequiredMixin, CreateAssignPermView): +class UserCreateView( + SuccessMessageMixin, + LoginRequiredMixin, + DjangoPermissionRequiredMixin, + CreateAssignPermView, +): """Create user""" model = User form_class = UserForm - permission_required = 'passbook_core.add_user' + permission_required = "passbook_core.add_user" - template_name = 'generic/create.html' - success_url = reverse_lazy('passbook_admin:users') - success_message = _('Successfully created User') + template_name = "generic/create.html" + success_url = reverse_lazy("passbook_admin:users") + success_message = _("Successfully created User") -class UserUpdateView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, UpdateView): +class UserUpdateView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView +): """Update user""" model = User form_class = UserForm - permission_required = 'passbook_core.change_user' + permission_required = "passbook_core.change_user" # By default the object's name is user which is used by other checks - context_object_name = 'object' - template_name = 'generic/update.html' - success_url = reverse_lazy('passbook_admin:users') - success_message = _('Successfully updated User') + context_object_name = "object" + template_name = "generic/update.html" + success_url = reverse_lazy("passbook_admin:users") + success_message = _("Successfully updated User") -class UserDeleteView(SuccessMessageMixin, LoginRequiredMixin, - PermissionRequiredMixin, DeleteView): +class UserDeleteView( + SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView +): """Delete user""" model = User - permission_required = 'passbook_core.delete_user' + permission_required = "passbook_core.delete_user" - template_name = 'generic/delete.html' - success_url = reverse_lazy('passbook_admin:users') - success_message = _('Successfully deleted User') + template_name = "generic/delete.html" + success_url = reverse_lazy("passbook_admin:users") + success_message = _("Successfully deleted User") def delete(self, request, *args, **kwargs): messages.success(self.request, self.success_message) @@ -73,13 +80,16 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV """Get Password reset link for user""" model = User - permission_required = 'passbook_core.reset_user_password' + permission_required = "passbook_core.reset_user_password" def get(self, request, *args, **kwargs): """Create nonce for user and return link""" super().get(request, *args, **kwargs) nonce = Nonce.objects.create(user=self.object) - link = request.build_absolute_uri(reverse( - 'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid})) - messages.success(request, _('Password reset link:
%(link)s
' % {'link': link})) - return redirect('passbook_admin:users') + link = request.build_absolute_uri( + reverse("passbook_core:auth-password-reset", kwargs={"nonce": nonce.uuid}) + ) + messages.success( + request, _("Password reset link:
%(link)s
" % {"link": link}) + ) + return redirect("passbook_admin:users") diff --git a/passbook/api/apps.py b/passbook/api/apps.py index 3da196c91..529d898a1 100644 --- a/passbook/api/apps.py +++ b/passbook/api/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig class PassbookAPIConfig(AppConfig): """passbook API Config""" - name = 'passbook.api' - label = 'passbook_api' - mountpoint = 'api/' - verbose_name = 'passbook API' + name = "passbook.api" + label = "passbook_api" + mountpoint = "api/" + verbose_name = "passbook API" diff --git a/passbook/api/permissions.py b/passbook/api/permissions.py index 7981903c9..90a7a02af 100644 --- a/passbook/api/permissions.py +++ b/passbook/api/permissions.py @@ -9,13 +9,13 @@ class CustomObjectPermissions(DjangoObjectPermissions): """Similar to `DjangoObjectPermissions`, but adding 'view' permissions.""" perms_map = { - 'GET': ['%(app_label)s.view_%(model_name)s'], - 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], - 'HEAD': ['%(app_label)s.view_%(model_name)s'], - 'POST': ['%(app_label)s.add_%(model_name)s'], - 'PUT': ['%(app_label)s.change_%(model_name)s'], - 'PATCH': ['%(app_label)s.change_%(model_name)s'], - 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + "GET": ["%(app_label)s.view_%(model_name)s"], + "OPTIONS": ["%(app_label)s.view_%(model_name)s"], + "HEAD": ["%(app_label)s.view_%(model_name)s"], + "POST": ["%(app_label)s.add_%(model_name)s"], + "PUT": ["%(app_label)s.change_%(model_name)s"], + "PATCH": ["%(app_label)s.change_%(model_name)s"], + "DELETE": ["%(app_label)s.delete_%(model_name)s"], } diff --git a/passbook/api/urls.py b/passbook/api/urls.py index 1685de242..2115b64b2 100644 --- a/passbook/api/urls.py +++ b/passbook/api/urls.py @@ -5,6 +5,6 @@ from passbook.api.v1.urls import urlpatterns as v1_urls from passbook.api.v2.urls import urlpatterns as v2_urls urlpatterns = [ - path('v1/', include(v1_urls)), - path('v2/', include(v2_urls)), + path("v1/", include(v1_urls)), + path("v2/", include(v2_urls)), ] diff --git a/passbook/api/v1/openid.py b/passbook/api/v1/openid.py index a928da179..df6b3181b 100644 --- a/passbook/api/v1/openid.py +++ b/passbook/api/v1/openid.py @@ -7,16 +7,16 @@ from oauth2_provider.views.mixins import ScopedResourceMixin class OpenIDUserInfoView(ScopedResourceMixin, View): """Passbook v1 OpenID API""" - required_scopes = ['openid:userinfo'] + required_scopes = ["openid:userinfo"] def get(self, request, *_, **__): """Passbook v1 OpenID API""" payload = { - 'sub': request.user.uuid.int, - 'name': request.user.get_full_name(), - 'given_name': request.user.name, - 'family_name': '', - 'preferred_username': request.user.username, - 'email': request.user.email, + "sub": request.user.uuid.int, + "name": request.user.get_full_name(), + "given_name": request.user.name, + "family_name": "", + "preferred_username": request.user.username, + "email": request.user.email, } return JsonResponse(payload) diff --git a/passbook/api/v1/urls.py b/passbook/api/v1/urls.py index 78edbd033..14adc4305 100644 --- a/passbook/api/v1/urls.py +++ b/passbook/api/v1/urls.py @@ -3,6 +3,4 @@ from django.urls import path from passbook.api.v1.openid import OpenIDUserInfoView -urlpatterns = [ - path('openid/', OpenIDUserInfoView.as_view(), name='openid') -] +urlpatterns = [path("openid/", OpenIDUserInfoView.as_view(), name="openid")] diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 2c898254b..a7b18b39c 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -34,70 +34,73 @@ from passbook.policies.webhook.api import WebhookPolicyViewSet from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet from passbook.providers.oauth.api import OAuth2ProviderViewSet from passbook.providers.oidc.api import OpenIDProviderViewSet -from passbook.providers.saml.api import (SAMLPropertyMappingViewSet, - SAMLProviderViewSet) -from passbook.sources.ldap.api import (LDAPPropertyMappingViewSet, - LDAPSourceViewSet) +from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet +from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet from passbook.sources.oauth.api import OAuthSourceViewSet LOGGER = get_logger() router = routers.DefaultRouter() for _passbook_app in get_apps(): - if hasattr(_passbook_app, 'api_mountpoint'): + if hasattr(_passbook_app, "api_mountpoint"): for prefix, viewset in _passbook_app.api_mountpoint: router.register(prefix, viewset) LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name) -router.register('core/applications', ApplicationViewSet) -router.register('core/invitations', InvitationViewSet) -router.register('core/groups', GroupViewSet) -router.register('core/users', UserViewSet) -router.register('audit/events', EventViewSet) -router.register('sources/all', SourceViewSet) -router.register('sources/ldap', LDAPSourceViewSet) -router.register('sources/oauth', OAuthSourceViewSet) -router.register('policies/all', PolicyViewSet) -router.register('policies/passwordexpiry', PasswordExpiryPolicyViewSet) -router.register('policies/groupmembership', GroupMembershipPolicyViewSet) -router.register('policies/haveibeenpwned', HaveIBeenPwendPolicyViewSet) -router.register('policies/fieldmatcher', FieldMatcherPolicyViewSet) -router.register('policies/password', PasswordPolicyViewSet) -router.register('policies/reputation', ReputationPolicyViewSet) -router.register('policies/ssologin', SSOLoginPolicyViewSet) -router.register('policies/webhook', WebhookPolicyViewSet) -router.register('providers/all', ProviderViewSet) -router.register('providers/applicationgateway', ApplicationGatewayProviderViewSet) -router.register('providers/oauth', OAuth2ProviderViewSet) -router.register('providers/openid', OpenIDProviderViewSet) -router.register('providers/saml', SAMLProviderViewSet) -router.register('propertymappings/all', PropertyMappingViewSet) -router.register('propertymappings/ldap', LDAPPropertyMappingViewSet) -router.register('propertymappings/saml', SAMLPropertyMappingViewSet) -router.register('factors/all', FactorViewSet) -router.register('factors/captcha', CaptchaFactorViewSet) -router.register('factors/dummy', DummyFactorViewSet) -router.register('factors/email', EmailFactorViewSet) -router.register('factors/otp', OTPFactorViewSet) -router.register('factors/password', PasswordFactorViewSet) +router.register("core/applications", ApplicationViewSet) +router.register("core/invitations", InvitationViewSet) +router.register("core/groups", GroupViewSet) +router.register("core/users", UserViewSet) +router.register("audit/events", EventViewSet) +router.register("sources/all", SourceViewSet) +router.register("sources/ldap", LDAPSourceViewSet) +router.register("sources/oauth", OAuthSourceViewSet) +router.register("policies/all", PolicyViewSet) +router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) +router.register("policies/groupmembership", GroupMembershipPolicyViewSet) +router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) +router.register("policies/fieldmatcher", FieldMatcherPolicyViewSet) +router.register("policies/password", PasswordPolicyViewSet) +router.register("policies/reputation", ReputationPolicyViewSet) +router.register("policies/ssologin", SSOLoginPolicyViewSet) +router.register("policies/webhook", WebhookPolicyViewSet) +router.register("providers/all", ProviderViewSet) +router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet) +router.register("providers/oauth", OAuth2ProviderViewSet) +router.register("providers/openid", OpenIDProviderViewSet) +router.register("providers/saml", SAMLProviderViewSet) +router.register("propertymappings/all", PropertyMappingViewSet) +router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) +router.register("propertymappings/saml", SAMLPropertyMappingViewSet) +router.register("factors/all", FactorViewSet) +router.register("factors/captcha", CaptchaFactorViewSet) +router.register("factors/dummy", DummyFactorViewSet) +router.register("factors/email", EmailFactorViewSet) +router.register("factors/otp", OTPFactorViewSet) +router.register("factors/password", PasswordFactorViewSet) info = openapi.Info( title="passbook API", - default_version='v2', + default_version="v2", # description="Test description", # terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="hello@beryju.org"), license=openapi.License(name="MIT License"), ) SchemaView = get_schema_view( - info, - public=True, - permission_classes=(CustomObjectPermissions,), + info, public=True, permission_classes=(CustomObjectPermissions,), ) urlpatterns = [ - url(r'^swagger(?P\.json|\.yaml)$', - SchemaView.without_ui(cache_timeout=0), name='schema-json'), - path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + url( + r"^swagger(?P\.json|\.yaml)$", + SchemaView.without_ui(cache_timeout=0), + name="schema-json", + ), + path( + "swagger/", + SchemaView.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), ] + router.urls diff --git a/passbook/audit/admin.py b/passbook/audit/admin.py index 1d67655ac..82a532499 100644 --- a/passbook/audit/admin.py +++ b/passbook/audit/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_audit') +admin_autoregister("passbook_audit") diff --git a/passbook/audit/api/events.py b/passbook/audit/api/events.py index f28c9df00..8c0652bcc 100644 --- a/passbook/audit/api/events.py +++ b/passbook/audit/api/events.py @@ -11,7 +11,16 @@ class EventSerializer(ModelSerializer): class Meta: model = Event - fields = ['pk', 'user', 'action', 'date', 'app', 'context', 'request_ip', 'created', ] + fields = [ + "pk", + "user", + "action", + "date", + "app", + "context", + "request_ip", + "created", + ] class EventViewSet(ReadOnlyModelViewSet): diff --git a/passbook/audit/apps.py b/passbook/audit/apps.py index cdb357d56..b0a9580d4 100644 --- a/passbook/audit/apps.py +++ b/passbook/audit/apps.py @@ -7,10 +7,10 @@ from django.apps import AppConfig class PassbookAuditConfig(AppConfig): """passbook audit app""" - name = 'passbook.audit' - label = 'passbook_audit' - verbose_name = 'passbook Audit' - mountpoint = 'audit/' + name = "passbook.audit" + label = "passbook_audit" + verbose_name = "passbook Audit" + mountpoint = "audit/" def ready(self): - import_module('passbook.audit.signals') + import_module("passbook.audit.signals") diff --git a/passbook/audit/migrations/0001_initial.py b/passbook/audit/migrations/0001_initial.py index 32d1bd814..77ade9f0c 100644 --- a/passbook/audit/migrations/0001_initial.py +++ b/passbook/audit/migrations/0001_initial.py @@ -18,20 +18,55 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='AuditEntry', + name="AuditEntry", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])), - ('date', models.DateTimeField(auto_now_add=True)), - ('app', models.TextField()), - ('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)), - ('request_ip', models.GenericIPAddressField()), - ('created', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "action", + models.TextField( + choices=[ + ("login", "login"), + ("login_failed", "login_failed"), + ("logout", "logout"), + ("authorize_application", "authorize_application"), + ("suspicious_request", "suspicious_request"), + ("sign_up", "sign_up"), + ("password_reset", "password_reset"), + ("invitation_created", "invitation_created"), + ("invitation_used", "invitation_used"), + ] + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ("app", models.TextField()), + ( + "context", + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, default=dict + ), + ), + ("request_ip", models.GenericIPAddressField()), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Audit Entry', - 'verbose_name_plural': 'Audit Entries', + "verbose_name": "Audit Entry", + "verbose_name_plural": "Audit Entries", }, ), ] diff --git a/passbook/audit/migrations/0002_auto_20191028_0829.py b/passbook/audit/migrations/0002_auto_20191028_0829.py index ce66f79b0..9a582528c 100644 --- a/passbook/audit/migrations/0002_auto_20191028_0829.py +++ b/passbook/audit/migrations/0002_auto_20191028_0829.py @@ -8,12 +8,9 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('passbook_audit', '0001_initial'), + ("passbook_audit", "0001_initial"), ] operations = [ - migrations.RenameModel( - old_name='AuditEntry', - new_name='Event', - ), + migrations.RenameModel(old_name="AuditEntry", new_name="Event",), ] diff --git a/passbook/audit/migrations/0003_auto_20191205_1407.py b/passbook/audit/migrations/0003_auto_20191205_1407.py index b57661317..61c7a4e77 100644 --- a/passbook/audit/migrations/0003_auto_20191205_1407.py +++ b/passbook/audit/migrations/0003_auto_20191205_1407.py @@ -8,17 +8,33 @@ import passbook.audit.models class Migration(migrations.Migration): dependencies = [ - ('passbook_audit', '0002_auto_20191028_0829'), + ("passbook_audit", "0002_auto_20191028_0829"), ] operations = [ migrations.AlterModelOptions( - name='event', - options={'verbose_name': 'Audit Event', 'verbose_name_plural': 'Audit Events'}, + name="event", + options={ + "verbose_name": "Audit Event", + "verbose_name_plural": "Audit Events", + }, ), migrations.AlterField( - model_name='event', - name='action', - field=models.TextField(choices=[('LOGIN', 'login'), ('LOGIN_FAILED', 'login_failed'), ('LOGOUT', 'logout'), ('AUTHORIZE_APPLICATION', 'authorize_application'), ('SUSPICIOUS_REQUEST', 'suspicious_request'), ('SIGN_UP', 'sign_up'), ('PASSWORD_RESET', 'password_reset'), ('INVITE_CREATED', 'invitation_created'), ('INVITE_USED', 'invitation_used'), ('CUSTOM', 'custom')]), + model_name="event", + name="action", + field=models.TextField( + choices=[ + ("LOGIN", "login"), + ("LOGIN_FAILED", "login_failed"), + ("LOGOUT", "logout"), + ("AUTHORIZE_APPLICATION", "authorize_application"), + ("SUSPICIOUS_REQUEST", "suspicious_request"), + ("SIGN_UP", "sign_up"), + ("PASSWORD_RESET", "password_reset"), + ("INVITE_CREATED", "invitation_created"), + ("INVITE_USED", "invitation_used"), + ("CUSTOM", "custom"), + ] + ), ), ] diff --git a/passbook/audit/migrations/0004_auto_20191205_1502.py b/passbook/audit/migrations/0004_auto_20191205_1502.py index 414c46934..bd45599eb 100644 --- a/passbook/audit/migrations/0004_auto_20191205_1502.py +++ b/passbook/audit/migrations/0004_auto_20191205_1502.py @@ -6,17 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_audit', '0003_auto_20191205_1407'), + ("passbook_audit", "0003_auto_20191205_1407"), ] operations = [ - migrations.RemoveField( - model_name='event', - name='request_ip', - ), + migrations.RemoveField(model_name="event", name="request_ip",), migrations.AddField( - model_name='event', - name='client_ip', + model_name="event", + name="client_ip", field=models.GenericIPAddressField(null=True), ), ] diff --git a/passbook/audit/models.py b/passbook/audit/models.py index 8fc4b6686..50b55aa89 100644 --- a/passbook/audit/models.py +++ b/passbook/audit/models.py @@ -18,19 +18,20 @@ from passbook.lib.utils.http import get_client_ip LOGGER = get_logger() + class EventAction(Enum): """All possible actions to save into the audit log""" - LOGIN = 'login' - LOGIN_FAILED = 'login_failed' - LOGOUT = 'logout' - AUTHORIZE_APPLICATION = 'authorize_application' - SUSPICIOUS_REQUEST = 'suspicious_request' - SIGN_UP = 'sign_up' - PASSWORD_RESET = 'password_reset' # noqa # nosec - INVITE_CREATED = 'invitation_created' - INVITE_USED = 'invitation_used' - CUSTOM = 'custom' + LOGIN = "login" + LOGIN_FAILED = "login_failed" + LOGOUT = "logout" + AUTHORIZE_APPLICATION = "authorize_application" + SUSPICIOUS_REQUEST = "suspicious_request" + SIGN_UP = "sign_up" + PASSWORD_RESET = "password_reset" # noqa # nosec + INVITE_CREATED = "invitation_created" + INVITE_USED = "invitation_used" + CUSTOM = "custom" @staticmethod def as_choices(): @@ -41,7 +42,9 @@ class EventAction(Enum): class Event(UUIDModel): """An individual audit log event""" - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL + ) action = models.TextField(choices=EventAction.as_choices()) date = models.DateTimeField(auto_now_add=True) app = models.TextField() @@ -56,28 +59,30 @@ class Event(UUIDModel): return request.resolver_match.app_name @staticmethod - def new(action: EventAction, - app: Optional[str] = None, - _inspect_offset: int = 1, - **kwargs) -> 'Event': + def new( + action: EventAction, + app: Optional[str] = None, + _inspect_offset: int = 1, + **kwargs, + ) -> "Event": """Create new Event instance from arguments. Instance is NOT saved.""" if not isinstance(action, EventAction): - raise ValueError(f"action must be EventAction instance but was {type(action)}") + raise ValueError( + f"action must be EventAction instance but was {type(action)}" + ) if not app: app = getmodule(stack()[_inspect_offset][0]).__name__ - event = Event( - action=action.value, - app=app, - context=kwargs) + event = Event(action=action.value, app=app, context=kwargs) LOGGER.debug("Created Audit event", action=action, context=kwargs) return event - def from_http(self, request: HttpRequest, - user: Optional[settings.AUTH_USER_MODEL] = None) -> 'Event': + def from_http( + self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None + ) -> "Event": """Add data from a Django-HttpRequest, allowing the creation of Events independently from requests. `user` arguments optionally overrides user from requests.""" - if hasattr(request, 'user'): + if hasattr(request, "user"): if isinstance(request.user, AnonymousUser): self.user = get_anonymous_user() else: @@ -85,7 +90,7 @@ class Event(UUIDModel): if user: self.user = user # User 255.255.255.255 as fallback if IP cannot be determined - self.client_ip = get_client_ip(request) or '255.255.255.255' + self.client_ip = get_client_ip(request) or "255.255.255.255" # If there's no app set, we get it from the requests too if not self.app: self.app = Event._get_app_from_request(request) @@ -94,10 +99,12 @@ class Event(UUIDModel): def save(self, *args, **kwargs): if not self._state.adding: - raise ValidationError("you may not edit an existing %s" % self._meta.model_name) + raise ValidationError( + "you may not edit an existing %s" % self._meta.model_name + ) return super().save(*args, **kwargs) class Meta: - verbose_name = _('Audit Event') - verbose_name_plural = _('Audit Events') + verbose_name = _("Audit Event") + verbose_name_plural = _("Audit Events") diff --git a/passbook/audit/signals.py b/passbook/audit/signals.py index cbfe171f5..b0106a3db 100644 --- a/passbook/audit/signals.py +++ b/passbook/audit/signals.py @@ -3,8 +3,7 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out from django.dispatch import receiver from passbook.audit.models import Event, EventAction -from passbook.core.signals import (invitation_created, invitation_used, - user_signed_up) +from passbook.core.signals import invitation_created, invitation_used, user_signed_up @receiver(user_logged_in) @@ -32,11 +31,15 @@ def on_user_signed_up(sender, request, user, **_): # pylint: disable=unused-argument def on_invitation_created(sender, request, invitation, **_): """Log Invitation creation""" - Event.new(EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex).from_http(request) + Event.new( + EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex + ).from_http(request) @receiver(invitation_used) # pylint: disable=unused-argument def on_invitation_used(sender, request, invitation, **_): """Log Invitation usage""" - Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(request) + Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http( + request + ) diff --git a/passbook/core/admin.py b/passbook/core/admin.py index 9f22234fc..6e91b2080 100644 --- a/passbook/core/admin.py +++ b/passbook/core/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_core') +admin_autoregister("passbook_core") diff --git a/passbook/core/api/applications.py b/passbook/core/api/applications.py index 843bb2c31..9e4930c46 100644 --- a/passbook/core/api/applications.py +++ b/passbook/core/api/applications.py @@ -11,8 +11,16 @@ class ApplicationSerializer(ModelSerializer): class Meta: model = Application - fields = ['pk', 'name', 'slug', 'launch_url', 'icon_url', - 'provider', 'policies', 'skip_authorization'] + fields = [ + "pk", + "name", + "slug", + "launch_url", + "icon_url", + "provider", + "policies", + "skip_authorization", + ] class ApplicationViewSet(ModelViewSet): diff --git a/passbook/core/api/factors.py b/passbook/core/api/factors.py index 3dd6029a7..ec812fa8b 100644 --- a/passbook/core/api/factors.py +++ b/passbook/core/api/factors.py @@ -8,16 +8,16 @@ from passbook.core.models import Factor class FactorSerializer(ModelSerializer): """Factor Serializer""" - __type__ = SerializerMethodField(method_name='get_type') + __type__ = SerializerMethodField(method_name="get_type") def get_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace('factor', '') + return obj._meta.object_name.lower().replace("factor", "") class Meta: model = Factor - fields = ['pk', 'name', 'slug', 'order', 'enabled', '__type__'] + fields = ["pk", "name", "slug", "order", "enabled", "__type__"] class FactorViewSet(ReadOnlyModelViewSet): diff --git a/passbook/core/api/groups.py b/passbook/core/api/groups.py index 1ce5d026e..fbd9fc7e0 100644 --- a/passbook/core/api/groups.py +++ b/passbook/core/api/groups.py @@ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer): class Meta: model = Group - fields = ['pk', 'name', 'parent', 'user_set', 'attributes'] + fields = ["pk", "name", "parent", "user_set", "attributes"] class GroupViewSet(ModelViewSet): diff --git a/passbook/core/api/invitations.py b/passbook/core/api/invitations.py index 618f0248f..c6e451f62 100644 --- a/passbook/core/api/invitations.py +++ b/passbook/core/api/invitations.py @@ -11,7 +11,13 @@ class InvitationSerializer(ModelSerializer): class Meta: model = Invitation - fields = ['pk', 'expires', 'fixed_username', 'fixed_email', 'needs_confirmation'] + fields = [ + "pk", + "expires", + "fixed_username", + "fixed_email", + "needs_confirmation", + ] class InvitationViewSet(ModelViewSet): diff --git a/passbook/core/api/policies.py b/passbook/core/api/policies.py index 461ccf0ae..bf154d063 100644 --- a/passbook/core/api/policies.py +++ b/passbook/core/api/policies.py @@ -9,16 +9,16 @@ from passbook.policies.forms import GENERAL_FIELDS class PolicySerializer(ModelSerializer): """Policy Serializer""" - __type__ = SerializerMethodField(method_name='get_type') + __type__ = SerializerMethodField(method_name="get_type") def get_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace('policy', '') + return obj._meta.object_name.lower().replace("policy", "") class Meta: model = Policy - fields = ['pk'] + GENERAL_FIELDS + ['__type__'] + fields = ["pk"] + GENERAL_FIELDS + ["__type__"] class PolicyViewSet(ReadOnlyModelViewSet): diff --git a/passbook/core/api/propertymappings.py b/passbook/core/api/propertymappings.py index f0d17c070..8567a49e6 100644 --- a/passbook/core/api/propertymappings.py +++ b/passbook/core/api/propertymappings.py @@ -8,16 +8,16 @@ from passbook.core.models import PropertyMapping class PropertyMappingSerializer(ModelSerializer): """PropertyMapping Serializer""" - __type__ = SerializerMethodField(method_name='get_type') + __type__ = SerializerMethodField(method_name="get_type") def get_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace('propertymapping', '') + return obj._meta.object_name.lower().replace("propertymapping", "") class Meta: model = PropertyMapping - fields = ['pk', 'name', '__type__'] + fields = ["pk", "name", "__type__"] class PropertyMappingViewSet(ReadOnlyModelViewSet): diff --git a/passbook/core/api/providers.py b/passbook/core/api/providers.py index 5465baa92..2b54a5807 100644 --- a/passbook/core/api/providers.py +++ b/passbook/core/api/providers.py @@ -8,16 +8,16 @@ from passbook.core.models import Provider class ProviderSerializer(ModelSerializer): """Provider Serializer""" - __type__ = SerializerMethodField(method_name='get_type') + __type__ = SerializerMethodField(method_name="get_type") def get_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace('provider', '') + return obj._meta.object_name.lower().replace("provider", "") class Meta: model = Provider - fields = ['pk', 'property_mappings', '__type__'] + fields = ["pk", "property_mappings", "__type__"] class ProviderViewSet(ReadOnlyModelViewSet): diff --git a/passbook/core/api/sources.py b/passbook/core/api/sources.py index 09ab7ccc7..0cc49212b 100644 --- a/passbook/core/api/sources.py +++ b/passbook/core/api/sources.py @@ -9,16 +9,16 @@ from passbook.core.models import Source class SourceSerializer(ModelSerializer): """Source Serializer""" - __type__ = SerializerMethodField(method_name='get_type') + __type__ = SerializerMethodField(method_name="get_type") def get_type(self, obj): """Get object type so that we know which API Endpoint to use to get the full object""" - return obj._meta.object_name.lower().replace('source', '') + return obj._meta.object_name.lower().replace("source", "") class Meta: model = Source - fields = SOURCE_SERIALIZER_FIELDS + ['__type__'] + fields = SOURCE_SERIALIZER_FIELDS + ["__type__"] class SourceViewSet(ReadOnlyModelViewSet): diff --git a/passbook/core/api/users.py b/passbook/core/api/users.py index b6cc3c05e..d583fbf5e 100644 --- a/passbook/core/api/users.py +++ b/passbook/core/api/users.py @@ -11,7 +11,7 @@ class UserSerializer(ModelSerializer): class Meta: model = User - fields = ['pk', 'username', 'name', 'email'] + fields = ["pk", "username", "name", "email"] class UserViewSet(ModelViewSet): diff --git a/passbook/core/apps.py b/passbook/core/apps.py index 5b96cfff2..9b827a5a3 100644 --- a/passbook/core/apps.py +++ b/passbook/core/apps.py @@ -5,7 +5,7 @@ from django.apps import AppConfig class PassbookCoreConfig(AppConfig): """passbook core app config""" - name = 'passbook.core' - label = 'passbook_core' - verbose_name = 'passbook Core' - mountpoint = '' + name = "passbook.core" + label = "passbook_core" + verbose_name = "passbook Core" + mountpoint = "" diff --git a/passbook/core/forms/applications.py b/passbook/core/forms/applications.py index 812cf724b..0d6756585 100644 --- a/passbook/core/forms/applications.py +++ b/passbook/core/forms/applications.py @@ -9,21 +9,29 @@ from passbook.core.models import Application, Provider class ApplicationForm(forms.ModelForm): """Application Form""" - provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses(), - required=False) + provider = forms.ModelChoiceField( + queryset=Provider.objects.all().select_subclasses(), required=False + ) class Meta: model = Application - fields = ['name', 'slug', 'launch_url', 'icon_url', - 'provider', 'policies', 'skip_authorization'] + fields = [ + "name", + "slug", + "launch_url", + "icon_url", + "provider", + "policies", + "skip_authorization", + ] widgets = { - 'name': forms.TextInput(), - 'launch_url': forms.TextInput(), - 'icon_url': forms.TextInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) + "name": forms.TextInput(), + "launch_url": forms.TextInput(), + "icon_url": forms.TextInput(), + "policies": FilteredSelectMultiple(_("policies"), False), } labels = { - 'launch_url': _('Launch URL'), - 'icon_url': _('Icon URL'), + "launch_url": _("Launch URL"), + "icon_url": _("Icon URL"), } diff --git a/passbook/core/forms/authentication.py b/passbook/core/forms/authentication.py index 81d90a5ed..38c29b36d 100644 --- a/passbook/core/forms/authentication.py +++ b/passbook/core/forms/authentication.py @@ -11,55 +11,64 @@ from passbook.lib.utils.ui import human_list LOGGER = get_logger() + class LoginForm(forms.Form): """Allow users to login""" - title = _('Log in to your account') + title = _("Log in to your account") uid_field = forms.CharField() remember_me = forms.BooleanField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if CONFIG.y('passbook.uid_fields') == ['e-mail']: - self.fields['uid_field'] = forms.EmailField() - self.fields['uid_field'].widget.attrs = { - 'placeholder': _(human_list([x.title() for x in CONFIG.y('passbook.uid_fields')])) + if CONFIG.y("passbook.uid_fields") == ["e-mail"]: + self.fields["uid_field"] = forms.EmailField() + self.fields["uid_field"].widget.attrs = { + "placeholder": _( + human_list([x.title() for x in CONFIG.y("passbook.uid_fields")]) + ) } def clean_uid_field(self): """Validate uid_field after EmailValidator if 'email' is the only selected uid_fields""" - if CONFIG.y('passbook.uid_fields') == ['email']: - validate_email(self.cleaned_data.get('uid_field')) - return self.cleaned_data.get('uid_field') + if CONFIG.y("passbook.uid_fields") == ["email"]: + validate_email(self.cleaned_data.get("uid_field")) + return self.cleaned_data.get("uid_field") class SignUpForm(forms.Form): """SignUp Form""" - title = _('Sign Up') - name = forms.CharField(label=_('Name'), - widget=forms.TextInput(attrs={'placeholder': _('Name')})) - username = forms.CharField(label=_('Username'), - widget=forms.TextInput(attrs={'placeholder': _('Username')})) - email = forms.EmailField(label=_('E-Mail'), - widget=forms.TextInput(attrs={'placeholder': _('E-Mail')})) - password = forms.CharField(label=_('Password'), - widget=forms.PasswordInput(attrs={'placeholder': _('Password')})) - password_repeat = forms.CharField(label=_('Repeat Password'), - widget=forms.PasswordInput(attrs={ - 'placeholder': _('Repeat Password') - })) + title = _("Sign Up") + name = forms.CharField( + label=_("Name"), widget=forms.TextInput(attrs={"placeholder": _("Name")}) + ) + username = forms.CharField( + label=_("Username"), + widget=forms.TextInput(attrs={"placeholder": _("Username")}), + ) + email = forms.EmailField( + label=_("E-Mail"), widget=forms.TextInput(attrs={"placeholder": _("E-Mail")}) + ) + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput(attrs={"placeholder": _("Password")}), + ) + password_repeat = forms.CharField( + label=_("Repeat Password"), + widget=forms.PasswordInput(attrs={"placeholder": _("Repeat Password")}), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # All fields which have initial data supplied are set to read only - if 'initial' in kwargs: - for field in kwargs.get('initial').keys(): - self.fields[field].widget.attrs['readonly'] = 'readonly' + if "initial" in kwargs: + for field in kwargs.get("initial").keys(): + self.fields[field].widget.attrs["readonly"] = "readonly" def clean_username(self): """Check if username is used already""" - username = self.cleaned_data.get('username') + username = self.cleaned_data.get("username") if User.objects.filter(username=username).exists(): LOGGER.warning("Username %s already exists", username) raise ValidationError(_("Username already exists")) @@ -67,7 +76,7 @@ class SignUpForm(forms.Form): def clean_email(self): """Check if email is already used in django or other auth sources""" - email = self.cleaned_data.get('email') + email = self.cleaned_data.get("email") # Check if user exists already, error early if User.objects.filter(email=email).exists(): LOGGER.debug("email %s exists in django", email) @@ -76,8 +85,8 @@ class SignUpForm(forms.Form): def clean_password_repeat(self): """Check if Password adheres to filter and if passwords matche""" - password = self.cleaned_data.get('password') - password_repeat = self.cleaned_data.get('password_repeat') + password = self.cleaned_data.get("password") + password_repeat = self.cleaned_data.get("password_repeat") if password != password_repeat: raise ValidationError(_("Passwords don't match")) - return self.cleaned_data.get('password_repeat') + return self.cleaned_data.get("password_repeat") diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py index e57240f09..46577d957 100644 --- a/passbook/core/forms/groups.py +++ b/passbook/core/forms/groups.py @@ -9,24 +9,29 @@ class GroupForm(forms.ModelForm): """Group Form""" members = forms.ModelMultipleChoiceField( - User.objects.all(), required=False, widget=FilteredSelectMultiple('users', False)) + User.objects.all(), + required=False, + widget=FilteredSelectMultiple("users", False), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: - self.initial['members'] = self.instance.user_set.values_list('pk', flat=True) + self.initial["members"] = self.instance.user_set.values_list( + "pk", flat=True + ) def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) if instance.pk: instance.user_set.clear() - instance.user_set.add(*self.cleaned_data['members']) + instance.user_set.add(*self.cleaned_data["members"]) return instance class Meta: model = Group - fields = ['name', 'parent', 'members', 'attributes'] + fields = ["name", "parent", "members", "attributes"] widgets = { - 'name': forms.TextInput(), + "name": forms.TextInput(), } diff --git a/passbook/core/forms/invitations.py b/passbook/core/forms/invitations.py index b6f990830..be64ce302 100644 --- a/passbook/core/forms/invitations.py +++ b/passbook/core/forms/invitations.py @@ -12,27 +12,27 @@ class InvitationForm(forms.ModelForm): def clean_fixed_username(self): """Check if username is already used""" - username = self.cleaned_data.get('fixed_username') + username = self.cleaned_data.get("fixed_username") if User.objects.filter(username=username).exists(): - raise ValidationError(_('Username is already in use.')) + raise ValidationError(_("Username is already in use.")) return username def clean_fixed_email(self): """Check if email is already used""" - email = self.cleaned_data.get('fixed_email') + email = self.cleaned_data.get("fixed_email") if User.objects.filter(email=email).exists(): - raise ValidationError(_('E-Mail is already in use.')) + raise ValidationError(_("E-Mail is already in use.")) return email class Meta: model = Invitation - fields = ['expires', 'fixed_username', 'fixed_email', 'needs_confirmation'] + fields = ["expires", "fixed_username", "fixed_email", "needs_confirmation"] labels = { - 'fixed_username': "Force user's username (optional)", - 'fixed_email': "Force user's email (optional)", + "fixed_username": "Force user's username (optional)", + "fixed_email": "Force user's email (optional)", } widgets = { - 'fixed_username': forms.TextInput(), - 'fixed_email': forms.TextInput(), + "fixed_username": forms.TextInput(), + "fixed_email": forms.TextInput(), } diff --git a/passbook/core/forms/policies.py b/passbook/core/forms/policies.py index d8ca19127..b2d9cb5e8 100644 --- a/passbook/core/forms/policies.py +++ b/passbook/core/forms/policies.py @@ -13,10 +13,8 @@ class DebugPolicyForm(forms.ModelForm): class Meta: model = DebugPolicy - fields = GENERAL_FIELDS + ['result', 'wait_min', 'wait_max'] + fields = GENERAL_FIELDS + ["result", "wait_min", "wait_max"] widgets = { - 'name': forms.TextInput(), - } - labels = { - 'result': _('Allow user') + "name": forms.TextInput(), } + labels = {"result": _("Allow user")} diff --git a/passbook/core/forms/users.py b/passbook/core/forms/users.py index a210fdad3..b989eff9a 100644 --- a/passbook/core/forms/users.py +++ b/passbook/core/forms/users.py @@ -13,29 +13,30 @@ class UserDetailForm(forms.ModelForm): class Meta: model = User - fields = ['username', 'name', 'email'] - widgets = { - 'name': forms.TextInput - } + fields = ["username", "name", "email"] + widgets = {"name": forms.TextInput} + class PasswordChangeForm(forms.Form): """Form to update password""" - password = forms.CharField(label=_('Password'), - widget=forms.PasswordInput(attrs={ - 'placeholder': _('New Password'), - 'autocomplete': 'new-password' - })) - password_repeat = forms.CharField(label=_('Repeat Password'), - widget=forms.PasswordInput(attrs={ - 'placeholder': _('Repeat Password'), - 'autocomplete': 'new-password' - })) + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput( + attrs={"placeholder": _("New Password"), "autocomplete": "new-password"} + ), + ) + password_repeat = forms.CharField( + label=_("Repeat Password"), + widget=forms.PasswordInput( + attrs={"placeholder": _("Repeat Password"), "autocomplete": "new-password"} + ), + ) def clean_password_repeat(self): """Check if Password adheres to filter and if passwords matche""" - password = self.cleaned_data.get('password') - password_repeat = self.cleaned_data.get('password_repeat') + password = self.cleaned_data.get("password") + password_repeat = self.cleaned_data.get("password_repeat") if password != password_repeat: raise ValidationError(_("Passwords don't match")) - return self.cleaned_data.get('password_repeat') + return self.cleaned_data.get("password_repeat") diff --git a/passbook/core/migrations/0001_initial.py b/passbook/core/migrations/0001_initial.py index bbc44d1dc..332c900cf 100644 --- a/passbook/core/migrations/0001_initial.py +++ b/passbook/core/migrations/0001_initial.py @@ -18,206 +18,433 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0011_update_proxy_permissions'), + ("auth", "0011_update_proxy_permissions"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), - ('name', models.TextField()), - ('password_change_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False)), + ("name", models.TextField()), + ("password_change_date", models.DateTimeField(auto_now_add=True)), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], + managers=[("objects", django.contrib.auth.models.UserManager()),], ), migrations.CreateModel( - name='Policy', + name="Policy", fields=[ - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.TextField(blank=True, null=True)), - ('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)), - ('negate', models.BooleanField(default=False)), - ('order', models.IntegerField(default=0)), - ('timeout', models.IntegerField(default=30)), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.TextField(blank=True, null=True)), + ( + "action", + models.CharField( + choices=[("allow", "allow"), ("deny", "deny")], max_length=20 + ), + ), + ("negate", models.BooleanField(default=False)), + ("order", models.IntegerField(default=0)), + ("timeout", models.IntegerField(default=30)), + ], + options={"abstract": False,}, + ), + migrations.CreateModel( + name="PolicyModel", + fields=[ + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "policies", + models.ManyToManyField(blank=True, to="passbook_core.Policy"), + ), + ], + options={"abstract": False,}, + ), + migrations.CreateModel( + name="PropertyMapping", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.TextField()), ], options={ - 'abstract': False, + "verbose_name": "Property Mapping", + "verbose_name_plural": "Property Mappings", }, ), migrations.CreateModel( - name='PolicyModel', + name="DebugPolicy", fields=[ - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("result", models.BooleanField(default=False)), + ("wait_min", models.IntegerField(default=5)), + ("wait_max", models.IntegerField(default=30)), ], options={ - 'abstract': False, + "verbose_name": "Debug Policy", + "verbose_name_plural": "Debug Policies", }, + bases=("passbook_core.policy",), ), migrations.CreateModel( - name='PropertyMapping', + name="Factor", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.TextField()), + ( + "policymodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PolicyModel", + ), + ), + ("name", models.TextField()), + ("slug", models.SlugField(unique=True)), + ("order", models.IntegerField()), + ("enabled", models.BooleanField(default=True)), ], - options={ - 'verbose_name': 'Property Mapping', - 'verbose_name_plural': 'Property Mappings', - }, + options={"abstract": False,}, + bases=("passbook_core.policymodel",), ), migrations.CreateModel( - name='DebugPolicy', + name="Source", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('result', models.BooleanField(default=False)), - ('wait_min', models.IntegerField(default=5)), - ('wait_max', models.IntegerField(default=30)), + ( + "policymodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PolicyModel", + ), + ), + ("name", models.TextField()), + ("slug", models.SlugField()), + ("enabled", models.BooleanField(default=True)), ], - options={ - 'verbose_name': 'Debug Policy', - 'verbose_name_plural': 'Debug Policies', - }, - bases=('passbook_core.policy',), + options={"abstract": False,}, + bases=("passbook_core.policymodel",), ), migrations.CreateModel( - name='Factor', + name="Provider", fields=[ - ('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')), - ('name', models.TextField()), - ('slug', models.SlugField(unique=True)), - ('order', models.IntegerField()), - ('enabled', models.BooleanField(default=True)), - ], - options={ - 'abstract': False, - }, - bases=('passbook_core.policymodel',), - ), - migrations.CreateModel( - name='Source', - fields=[ - ('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')), - ('name', models.TextField()), - ('slug', models.SlugField()), - ('enabled', models.BooleanField(default=True)), - ], - options={ - 'abstract': False, - }, - bases=('passbook_core.policymodel',), - ), - migrations.CreateModel( - name='Provider', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('property_mappings', models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "property_mappings", + models.ManyToManyField( + blank=True, default=None, to="passbook_core.PropertyMapping" + ), + ), ], ), migrations.CreateModel( - name='Nonce', + name="Nonce", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)), - ('expiring', models.BooleanField(default=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "expires", + models.DateTimeField( + default=passbook.core.models.default_nonce_duration + ), + ), + ("expiring", models.BooleanField(default=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"verbose_name": "Nonce", "verbose_name_plural": "Nonces",}, + ), + migrations.CreateModel( + name="Invitation", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("expires", models.DateTimeField(blank=True, default=None, null=True)), + ("fixed_username", models.TextField(blank=True, default=None)), + ("fixed_email", models.TextField(blank=True, default=None)), + ("needs_confirmation", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Nonce', - 'verbose_name_plural': 'Nonces', + "verbose_name": "Invitation", + "verbose_name_plural": "Invitations", }, ), migrations.CreateModel( - name='Invitation', + name="Group", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('expires', models.DateTimeField(blank=True, default=None, null=True)), - ('fixed_username', models.TextField(blank=True, default=None)), - ('fixed_email', models.TextField(blank=True, default=None)), - ('needs_confirmation', models.BooleanField(default=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(max_length=80, verbose_name="name")), + ( + "tags", + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, default=dict + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="children", + to="passbook_core.Group", + ), + ), ], - options={ - 'verbose_name': 'Invitation', - 'verbose_name_plural': 'Invitations', - }, - ), - migrations.CreateModel( - name='Group', - fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=80, verbose_name='name')), - ('tags', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')), - ], - options={ - 'unique_together': {('name', 'parent')}, - }, + options={"unique_together": {("name", "parent")},}, ), migrations.AddField( - model_name='user', - name='groups', - field=models.ManyToManyField(to='passbook_core.Group'), + model_name="user", + name="groups", + field=models.ManyToManyField(to="passbook_core.Group"), ), migrations.AddField( - model_name='user', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + model_name="user", + name="user_permissions", + field=models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), ), migrations.CreateModel( - name='UserSourceConnection', + name="UserSourceConnection", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "source", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="passbook_core.Source", + ), + ), ], - options={ - 'unique_together': {('user', 'source')}, - }, + options={"unique_together": {("user", "source")},}, ), migrations.CreateModel( - name='Application', + name="Application", fields=[ - ('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')), - ('name', models.TextField()), - ('slug', models.SlugField()), - ('launch_url', models.URLField(blank=True, null=True)), - ('icon_url', models.TextField(blank=True, null=True)), - ('skip_authorization', models.BooleanField(default=False)), - ('provider', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider')), + ( + "policymodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PolicyModel", + ), + ), + ("name", models.TextField()), + ("slug", models.SlugField()), + ("launch_url", models.URLField(blank=True, null=True)), + ("icon_url", models.TextField(blank=True, null=True)), + ("skip_authorization", models.BooleanField(default=False)), + ( + "provider", + models.OneToOneField( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="passbook_core.Provider", + ), + ), ], - options={ - 'abstract': False, - }, - bases=('passbook_core.policymodel',), + options={"abstract": False,}, + bases=("passbook_core.policymodel",), ), migrations.AddField( - model_name='user', - name='sources', - field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'), + model_name="user", + name="sources", + field=models.ManyToManyField( + through="passbook_core.UserSourceConnection", to="passbook_core.Source" + ), ), ] diff --git a/passbook/core/migrations/0002_auto_20191010_1058.py b/passbook/core/migrations/0002_auto_20191010_1058.py index 76cd446ba..c87a5000b 100644 --- a/passbook/core/migrations/0002_auto_20191010_1058.py +++ b/passbook/core/migrations/0002_auto_20191010_1058.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='user', - options={'permissions': (('reset_user_password', 'Reset Password'),)}, + name="user", + options={"permissions": (("reset_user_password", "Reset Password"),)}, ), ] diff --git a/passbook/core/migrations/0002_nonce_description.py b/passbook/core/migrations/0002_nonce_description.py index 2e98fe1b8..233800827 100644 --- a/passbook/core/migrations/0002_nonce_description.py +++ b/passbook/core/migrations/0002_nonce_description.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.AddField( - model_name='nonce', - name='description', - field=models.TextField(blank=True, default=''), + model_name="nonce", + name="description", + field=models.TextField(blank=True, default=""), ), ] diff --git a/passbook/core/migrations/0003_auto_20191011_0914.py b/passbook/core/migrations/0003_auto_20191011_0914.py index 8321f1a0b..c39d48198 100644 --- a/passbook/core/migrations/0003_auto_20191011_0914.py +++ b/passbook/core/migrations/0003_auto_20191011_0914.py @@ -7,23 +7,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0002_nonce_description'), + ("passbook_core", "0002_nonce_description"), ] operations = [ migrations.RenameField( - model_name='group', - old_name='tags', - new_name='attributes', + model_name="group", old_name="tags", new_name="attributes", ), migrations.AddField( - model_name='source', - name='property_mappings', - field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'), + model_name="source", + name="property_mappings", + field=models.ManyToManyField( + blank=True, default=None, to="passbook_core.PropertyMapping" + ), ), migrations.AddField( - model_name='user', - name='attributes', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + model_name="user", + name="attributes", + field=django.contrib.postgres.fields.jsonb.JSONField( + blank=True, default=dict + ), ), ] diff --git a/passbook/core/migrations/0003_merge_20191010_1541.py b/passbook/core/migrations/0003_merge_20191010_1541.py index 1ef2e4560..26b7a3a69 100644 --- a/passbook/core/migrations/0003_merge_20191010_1541.py +++ b/passbook/core/migrations/0003_merge_20191010_1541.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0002_auto_20191010_1058'), - ('passbook_core', '0002_nonce_description'), + ("passbook_core", "0002_auto_20191010_1058"), + ("passbook_core", "0002_nonce_description"), ] - operations = [ - ] + operations = [] diff --git a/passbook/core/migrations/0004_remove_policy_action.py b/passbook/core/migrations/0004_remove_policy_action.py index 8e1b35b0c..9c432fe15 100644 --- a/passbook/core/migrations/0004_remove_policy_action.py +++ b/passbook/core/migrations/0004_remove_policy_action.py @@ -6,12 +6,9 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0003_auto_20191011_0914'), + ("passbook_core", "0003_auto_20191011_0914"), ] operations = [ - migrations.RemoveField( - model_name='policy', - name='action', - ), + migrations.RemoveField(model_name="policy", name="action",), ] diff --git a/passbook/core/migrations/0005_merge_20191025_2022.py b/passbook/core/migrations/0005_merge_20191025_2022.py index 609b5792b..8e6c7965f 100644 --- a/passbook/core/migrations/0005_merge_20191025_2022.py +++ b/passbook/core/migrations/0005_merge_20191025_2022.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0004_remove_policy_action'), - ('passbook_core', '0003_merge_20191010_1541'), + ("passbook_core", "0004_remove_policy_action"), + ("passbook_core", "0003_merge_20191010_1541"), ] - operations = [ - ] + operations = [] diff --git a/passbook/core/models.py b/passbook/core/models.py index 6d2332e6d..bca956284 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -27,12 +27,18 @@ def default_nonce_duration(): """Default duration a Nonce is valid""" return now() + timedelta(hours=4) + class Group(UUIDModel): """Custom Group model which supports a basic hierarchy""" - name = models.CharField(_('name'), max_length=80) - parent = models.ForeignKey('Group', blank=True, null=True, - on_delete=models.SET_NULL, related_name='children') + name = models.CharField(_("name"), max_length=80) + parent = models.ForeignKey( + "Group", + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name="children", + ) attributes = JSONField(default=dict, blank=True) def __str__(self): @@ -40,7 +46,8 @@ class Group(UUIDModel): class Meta: - unique_together = (('name', 'parent',),) + unique_together = (("name", "parent",),) + class User(GuardianUserMixin, AbstractUser): """Custom User model to allow easier adding o f user-based settings""" @@ -48,8 +55,8 @@ class User(GuardianUserMixin, AbstractUser): uuid = models.UUIDField(default=uuid4, editable=False) name = models.TextField() - sources = models.ManyToManyField('Source', through='UserSourceConnection') - groups = models.ManyToManyField('Group') + sources = models.ManyToManyField("Source", through="UserSourceConnection") + groups = models.ManyToManyField("Group") password_change_date = models.DateTimeField(auto_now_add=True) attributes = JSONField(default=dict, blank=True) @@ -62,28 +69,29 @@ class User(GuardianUserMixin, AbstractUser): class Meta: - permissions = ( - ('reset_user_password', 'Reset Password'), - ) + permissions = (("reset_user_password", "Reset Password"),) + class Provider(models.Model): """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" - property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True) + property_mappings = models.ManyToManyField( + "PropertyMapping", default=None, blank=True + ) objects = InheritanceManager() # This class defines no field for easier inheritance def __str__(self): - if hasattr(self, 'name'): - return getattr(self, 'name') + if hasattr(self, "name"): + return getattr(self, "name") return super().__str__() class PolicyModel(UUIDModel, CreatedUpdatedModel): """Base model which can have policies applied to it""" - policies = models.ManyToManyField('Policy', blank=True) + policies = models.ManyToManyField("Policy", blank=True) class UserSettings: @@ -108,8 +116,8 @@ class Factor(PolicyModel): enabled = models.BooleanField(default=True) objects = InheritanceManager() - type = '' - form = '' + type = "" + form = "" def user_settings(self) -> Optional[UserSettings]: """Entrypoint to integrate with User settings. Can either return None if no @@ -129,8 +137,9 @@ class Application(PolicyModel): slug = models.SlugField() launch_url = models.URLField(null=True, blank=True) icon_url = models.TextField(null=True, blank=True) - provider = models.OneToOneField('Provider', null=True, blank=True, - default=None, on_delete=models.SET_DEFAULT) + provider = models.OneToOneField( + "Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT + ) skip_authorization = models.BooleanField(default=False) objects = InheritanceManager() @@ -151,9 +160,11 @@ class Source(PolicyModel): name = models.TextField() slug = models.SlugField() enabled = models.BooleanField(default=True) - property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True) + property_mappings = models.ManyToManyField( + "PropertyMapping", default=None, blank=True + ) - form = '' # ModelForm-based class ued to create/edit instance + form = "" # ModelForm-based class ued to create/edit instance objects = InheritanceManager() @@ -185,7 +196,7 @@ class UserSourceConnection(CreatedUpdatedModel): class Meta: - unique_together = (('user', 'source'),) + unique_together = (("user", "source"),) class Policy(UUIDModel, CreatedUpdatedModel): @@ -215,25 +226,25 @@ class DebugPolicy(Policy): wait_min = models.IntegerField(default=5) wait_max = models.IntegerField(default=30) - form = 'passbook.core.forms.policies.DebugPolicyForm' + form = "passbook.core.forms.policies.DebugPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: """Wait random time then return result""" wait = SystemRandom().randrange(self.wait_min, self.wait_max) LOGGER.debug("Policy waiting", policy=self, delay=wait) sleep(wait) - return PolicyResult(self.result, 'Debugging') + return PolicyResult(self.result, "Debugging") class Meta: - verbose_name = _('Debug Policy') - verbose_name_plural = _('Debug Policies') + verbose_name = _("Debug Policy") + verbose_name_plural = _("Debug Policies") class Invitation(UUIDModel): """Single-use invitation link""" - created_by = models.ForeignKey('User', on_delete=models.CASCADE) + created_by = models.ForeignKey("User", on_delete=models.CASCADE) expires = models.DateTimeField(default=None, blank=True, null=True) fixed_username = models.TextField(blank=True, default=None) fixed_email = models.TextField(blank=True, default=None) @@ -242,24 +253,26 @@ class Invitation(UUIDModel): @property def link(self): """Get link to use invitation""" - return reverse_lazy('passbook_core:auth-sign-up') + f'?invitation={self.uuid.hex}' + return ( + reverse_lazy("passbook_core:auth-sign-up") + f"?invitation={self.uuid.hex}" + ) def __str__(self): return f"Invitation {self.uuid.hex} created by {self.created_by}" class Meta: - verbose_name = _('Invitation') - verbose_name_plural = _('Invitations') + verbose_name = _("Invitation") + verbose_name_plural = _("Invitations") class Nonce(UUIDModel): """One-time link for password resets/sign-up-confirmations""" expires = models.DateTimeField(default=default_nonce_duration) - user = models.ForeignKey('User', on_delete=models.CASCADE) + user = models.ForeignKey("User", on_delete=models.CASCADE) expiring = models.BooleanField(default=True) - description = models.TextField(default='', blank=True) + description = models.TextField(default="", blank=True) @property def is_expired(self) -> bool: @@ -271,8 +284,8 @@ class Nonce(UUIDModel): class Meta: - verbose_name = _('Nonce') - verbose_name_plural = _('Nonces') + verbose_name = _("Nonce") + verbose_name_plural = _("Nonces") class PropertyMapping(UUIDModel): @@ -280,7 +293,7 @@ class PropertyMapping(UUIDModel): name = models.TextField() - form = '' + form = "" objects = InheritanceManager() def __str__(self): @@ -288,5 +301,5 @@ class PropertyMapping(UUIDModel): class Meta: - verbose_name = _('Property Mapping') - verbose_name_plural = _('Property Mappings') + verbose_name = _("Property Mapping") + verbose_name_plural = _("Property Mappings") diff --git a/passbook/core/signals.py b/passbook/core/signals.py index 4dea5a5a7..7894bb4d6 100644 --- a/passbook/core/signals.py +++ b/passbook/core/signals.py @@ -7,10 +7,10 @@ from structlog import get_logger LOGGER = get_logger() -user_signed_up = Signal(providing_args=['request', 'user']) -invitation_created = Signal(providing_args=['request', 'invitation']) -invitation_used = Signal(providing_args=['request', 'invitation', 'user']) -password_changed = Signal(providing_args=['user', 'password']) +user_signed_up = Signal(providing_args=["request", "user"]) +invitation_created = Signal(providing_args=["request", "invitation"]) +invitation_used = Signal(providing_args=["request", "invitation", "user"]) +password_changed = Signal(providing_args=["user", "password"]) @receiver(post_save) @@ -18,6 +18,7 @@ password_changed = Signal(providing_args=['user', 'password']) def invalidate_policy_cache(sender, instance, **_): """Invalidate Policy cache when policy is updated""" from passbook.core.models import Policy + if isinstance(instance, Policy): LOGGER.debug("Invalidating policy cache", policy=instance) keys = cache.keys("%s#*" % instance.pk) diff --git a/passbook/core/tasks.py b/passbook/core/tasks.py index 2b14af046..e0c8898d8 100644 --- a/passbook/core/tasks.py +++ b/passbook/core/tasks.py @@ -7,8 +7,9 @@ from passbook.root.celery import CELERY_APP LOGGER = get_logger() + @CELERY_APP.task() def clean_nonces(): """Remove expired nonces""" amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete() - LOGGER.debug('Deleted expired nonces', amount=amount) + LOGGER.debug("Deleted expired nonces", amount=amount) diff --git a/passbook/core/templatetags/passbook_user_settings.py b/passbook/core/templatetags/passbook_user_settings.py index 3469ab729..82c7ec60c 100644 --- a/passbook/core/templatetags/passbook_user_settings.py +++ b/passbook/core/templatetags/passbook_user_settings.py @@ -9,29 +9,37 @@ from passbook.policies.engine import PolicyEngine register = template.Library() + @register.simple_tag(takes_context=True) def user_factors(context: RequestContext) -> List[UserSettings]: """Return list of all factors which apply to user""" - user = context.get('request').user - _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() + user = context.get("request").user + _all_factors = ( + Factor.objects.filter(enabled=True).order_by("order").select_subclasses() + ) matching_factors: List[UserSettings] = [] for factor in _all_factors: user_settings = factor.user_settings() - policy_engine = PolicyEngine(factor.policies.all(), user, context.get('request')) + policy_engine = PolicyEngine( + factor.policies.all(), user, context.get("request") + ) policy_engine.build() if policy_engine.passing and user_settings: matching_factors.append(user_settings) return matching_factors + @register.simple_tag(takes_context=True) def user_sources(context: RequestContext) -> List[UserSettings]: """Return a list of all sources which are enabled for the user""" - user = context.get('request').user + user = context.get("request").user _all_sources = Source.objects.filter(enabled=True).select_subclasses() matching_sources: List[UserSettings] = [] for factor in _all_sources: user_settings = factor.user_settings() - policy_engine = PolicyEngine(factor.policies.all(), user, context.get('request')) + policy_engine = PolicyEngine( + factor.policies.all(), user, context.get("request") + ) policy_engine.build() if policy_engine.passing and user_settings: matching_sources.append(user_settings) diff --git a/passbook/core/tests/test_views_authentication.py b/passbook/core/tests/test_views_authentication.py index 994ea0e44..4dd0b0954 100644 --- a/passbook/core/tests/test_views_authentication.py +++ b/passbook/core/tests/test_views_authentication.py @@ -15,70 +15,78 @@ class TestAuthenticationViews(TestCase): def setUp(self): super().setUp() self.sign_up_data = { - 'name': 'Test', - 'username': 'beryjuorg', - 'email': 'unittest@passbook.beryju.org', - 'password': 'B3ryju0rg!', - 'password_repeat': 'B3ryju0rg!', + "name": "Test", + "username": "beryjuorg", + "email": "unittest@passbook.beryju.org", + "password": "B3ryju0rg!", + "password_repeat": "B3ryju0rg!", } self.login_data = { - 'uid_field': 'unittest@example.com', + "uid_field": "unittest@example.com", } self.user = User.objects.create_superuser( - username='unittest user', - email='unittest@example.com', - password=''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(8))) - + username="unittest user", + email="unittest@example.com", + password="".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ), + ) def test_sign_up_view(self): """Test account.sign_up view (Anonymous)""" self.client.logout() - response = self.client.get(reverse('passbook_core:auth-sign-up')) + response = self.client.get(reverse("passbook_core:auth-sign-up")) self.assertEqual(response.status_code, 200) def test_login_view(self): """Test account.login view (Anonymous)""" self.client.logout() - response = self.client.get(reverse('passbook_core:auth-login')) + response = self.client.get(reverse("passbook_core:auth-login")) self.assertEqual(response.status_code, 200) # test login with post form = LoginForm(self.login_data) self.assertTrue(form.is_valid()) - response = self.client.post(reverse('passbook_core:auth-login'), data=form.cleaned_data) + response = self.client.post( + reverse("passbook_core:auth-login"), data=form.cleaned_data + ) self.assertEqual(response.status_code, 302) def test_logout_view(self): """Test account.logout view""" self.client.force_login(self.user) - response = self.client.get(reverse('passbook_core:auth-logout')) + response = self.client.get(reverse("passbook_core:auth-logout")) self.assertEqual(response.status_code, 302) def test_sign_up_view_auth(self): """Test account.sign_up view (Authenticated)""" self.client.force_login(self.user) - response = self.client.get(reverse('passbook_core:auth-logout')) + response = self.client.get(reverse("passbook_core:auth-logout")) self.assertEqual(response.status_code, 302) def test_login_view_auth(self): """Test account.login view (Authenticated)""" self.client.force_login(self.user) - response = self.client.get(reverse('passbook_core:auth-login')) + response = self.client.get(reverse("passbook_core:auth-login")) self.assertEqual(response.status_code, 302) def test_login_view_post(self): """Test account.login view POST (Anonymous)""" - login_response = self.client.post(reverse('passbook_core:auth-login'), data=self.login_data) + login_response = self.client.post( + reverse("passbook_core:auth-login"), data=self.login_data + ) self.assertEqual(login_response.status_code, 302) - self.assertEqual(login_response.url, reverse('passbook_core:auth-process')) + self.assertEqual(login_response.url, reverse("passbook_core:auth-process")) def test_sign_up_view_post(self): """Test account.sign_up view POST (Anonymous)""" form = SignUpForm(self.sign_up_data) self.assertTrue(form.is_valid()) - response = self.client.post(reverse('passbook_core:auth-sign-up'), data=form.cleaned_data) + response = self.client.post( + reverse("passbook_core:auth-sign-up"), data=form.cleaned_data + ) self.assertEqual(response.status_code, 302) # def test_reset_password_init_view(self): diff --git a/passbook/core/tests/test_views_overview.py b/passbook/core/tests/test_views_overview.py index 0cacfbcfd..4a857e2d4 100644 --- a/passbook/core/tests/test_views_overview.py +++ b/passbook/core/tests/test_views_overview.py @@ -14,12 +14,17 @@ class TestOverviewViews(TestCase): def setUp(self): super().setUp() self.user = User.objects.create_superuser( - username='unittest user', - email='unittest@example.com', - password=''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(8))) + username="unittest user", + email="unittest@example.com", + password="".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ), + ) self.client.force_login(self.user) def test_overview(self): """Test UserSettingsView""" - self.assertEqual(self.client.get(reverse('passbook_core:overview')).status_code, 200) + self.assertEqual( + self.client.get(reverse("passbook_core:overview")).status_code, 200 + ) diff --git a/passbook/core/tests/test_views_user.py b/passbook/core/tests/test_views_user.py index 4bba5fa78..baaef4b74 100644 --- a/passbook/core/tests/test_views_user.py +++ b/passbook/core/tests/test_views_user.py @@ -15,33 +15,43 @@ class TestUserViews(TestCase): def setUp(self): super().setUp() self.user = User.objects.create_superuser( - username='unittest user', - email='unittest@example.com', - password=''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(8))) + username="unittest user", + email="unittest@example.com", + password="".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ), + ) self.client.force_login(self.user) def test_user_settings(self): """Test UserSettingsView""" - self.assertEqual(self.client.get(reverse('passbook_core:user-settings')).status_code, 200) + self.assertEqual( + self.client.get(reverse("passbook_core:user-settings")).status_code, 200 + ) def test_user_delete(self): """Test UserDeleteView""" - self.assertEqual(self.client.post(reverse('passbook_core:user-delete')).status_code, 302) - self.assertEqual(User.objects.filter(username='unittest user').exists(), False) + self.assertEqual( + self.client.post(reverse("passbook_core:user-delete")).status_code, 302 + ) + self.assertEqual(User.objects.filter(username="unittest user").exists(), False) self.setUp() def test_user_change_password(self): """Test UserChangePasswordView""" - form_data = { - 'password': 'test2', - 'password_repeat': 'test2' - } + form_data = {"password": "test2", "password_repeat": "test2"} form = PasswordChangeForm(data=form_data) self.assertTrue(form.is_valid()) - self.assertEqual(self.client.get( - reverse('passbook_core:user-change-password')).status_code, 200) - self.assertEqual(self.client.post( - reverse('passbook_core:user-change-password'), data=form_data).status_code, 302) + self.assertEqual( + self.client.get(reverse("passbook_core:user-change-password")).status_code, + 200, + ) + self.assertEqual( + self.client.post( + reverse("passbook_core:user-change-password"), data=form_data + ).status_code, + 302, + ) self.user.refresh_from_db() - self.assertTrue(self.user.check_password('test2')) + self.assertTrue(self.user.check_password("test2")) diff --git a/passbook/core/tests/test_views_utils.py b/passbook/core/tests/test_views_utils.py index f63a315b1..3d0083425 100644 --- a/passbook/core/tests/test_views_utils.py +++ b/passbook/core/tests/test_views_utils.py @@ -13,22 +13,25 @@ class TestUtilViews(TestCase): def setUp(self): self.user = User.objects.create_superuser( - username='unittest user', - email='unittest@example.com', - password=''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(8))) + username="unittest user", + email="unittest@example.com", + password="".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ), + ) self.factory = RequestFactory() def test_loading_view(self): """Test loading view""" - request = self.factory.get('something') - response = LoadingView.as_view(target_url='somestring')(request) + request = self.factory.get("something") + response = LoadingView.as_view(target_url="somestring")(request) response.render() - self.assertIn('somestring', response.content.decode('utf-8')) + self.assertIn("somestring", response.content.decode("utf-8")) def test_permission_denied_view(self): """Test PermissionDeniedView""" - request = self.factory.get('something') + request = self.factory.get("something") request.user = self.user response = PermissionDeniedView.as_view()(request) self.assertEqual(response.status_code, 200) diff --git a/passbook/core/urls.py b/passbook/core/urls.py index 7857a41e0..154cc12f1 100644 --- a/passbook/core/urls.py +++ b/passbook/core/urls.py @@ -9,21 +9,38 @@ LOGGER = get_logger() urlpatterns = [ # Authentication views - path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), - path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'), - path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'), - path('auth/sign_up//confirm/', authentication.SignUpConfirmView.as_view(), - name='auth-sign-up-confirm'), - path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'), - path('auth/password/reset//', authentication.PasswordResetView.as_view(), - name='auth-password-reset'), - path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'), - path('auth/process//', view.AuthenticationView.as_view(), name='auth-process'), + path("auth/login/", authentication.LoginView.as_view(), name="auth-login"), + path("auth/logout/", authentication.LogoutView.as_view(), name="auth-logout"), + path("auth/sign_up/", authentication.SignUpView.as_view(), name="auth-sign-up"), + path( + "auth/sign_up//confirm/", + authentication.SignUpConfirmView.as_view(), + name="auth-sign-up-confirm", + ), + path( + "auth/process/denied/", + view.FactorPermissionDeniedView.as_view(), + name="auth-denied", + ), + path( + "auth/password/reset//", + authentication.PasswordResetView.as_view(), + name="auth-password-reset", + ), + path("auth/process/", view.AuthenticationView.as_view(), name="auth-process"), + path( + "auth/process//", + view.AuthenticationView.as_view(), + name="auth-process", + ), # User views - path('_/user/', user.UserSettingsView.as_view(), name='user-settings'), - path('_/user/delete/', user.UserDeleteView.as_view(), name='user-delete'), - path('_/user/change_password/', user.UserChangePasswordView.as_view(), - name='user-change-password'), + path("_/user/", user.UserSettingsView.as_view(), name="user-settings"), + path("_/user/delete/", user.UserDeleteView.as_view(), name="user-delete"), + path( + "_/user/change_password/", + user.UserChangePasswordView.as_view(), + name="user-change-password", + ), # Overview - path('', overview.OverviewView.as_view(), name='overview'), + path("", overview.OverviewView.as_view(), name="overview"), ] diff --git a/passbook/core/views/access.py b/passbook/core/views/access.py index 14a14d5ef..ff3888645 100644 --- a/passbook/core/views/access.py +++ b/passbook/core/views/access.py @@ -11,6 +11,7 @@ from passbook.policies.engine import PolicyEngine LOGGER = get_logger() + class AccessMixin: """Mixin class for usage in Authorization views. Provider functions to check application access, etc""" @@ -23,12 +24,18 @@ class AccessMixin: try: return provider.application except Application.DoesNotExist as exc: - messages.error(self.request, _('Provider "%(name)s" has no application assigned' % { - 'name': provider - })) + messages.error( + self.request, + _( + 'Provider "%(name)s" has no application assigned' + % {"name": provider} + ), + ) raise exc - def user_has_access(self, application: Application, user: User) -> Tuple[bool, List[str]]: + def user_has_access( + self, application: Application, user: User + ) -> Tuple[bool, List[str]]: """Check if user has access to application.""" LOGGER.debug("Checking permissions", user=user, application=application) policy_engine = PolicyEngine(application.policies.all(), user, self.request) diff --git a/passbook/core/views/authentication.py b/passbook/core/views/authentication.py index 86fe12633..49dad3742 100644 --- a/passbook/core/views/authentication.py +++ b/passbook/core/views/authentication.py @@ -25,41 +25,41 @@ LOGGER = get_logger() class LoginView(UserPassesTestMixin, FormView): """Allow users to sign in""" - template_name = 'login/form.html' + template_name = "login/form.html" form_class = LoginForm - success_url = '.' + success_url = "." # Allow only not authenticated users to login def test_func(self): return self.request.user.is_authenticated is False def handle_no_permission(self): - if 'next' in self.request.GET: - return redirect(self.request.GET.get('next')) - return redirect(reverse('passbook_core:overview')) + if "next" in self.request.GET: + return redirect(self.request.GET.get("next")) + return redirect(reverse("passbook_core:overview")) def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.y('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Log in to your account') - kwargs['primary_action'] = _('Log in') - kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled') - kwargs['sources'] = [] + kwargs["config"] = CONFIG.y("passbook") + kwargs["is_login"] = True + kwargs["title"] = _("Log in to your account") + kwargs["primary_action"] = _("Log in") + kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled") + kwargs["sources"] = [] sources = Source.objects.filter(enabled=True).select_subclasses() for source in sources: login_button = source.login_button if login_button: - kwargs['sources'].append(login_button) - if kwargs['sources']: - self.template_name = 'login/with_sources.html' + kwargs["sources"].append(login_button) + if kwargs["sources"]: + self.template_name = "login/with_sources.html" return super().get_context_data(**kwargs) def get_user(self, uid_value) -> Optional[User]: """Find user instance. Returns None if no user was found.""" - for search_field in CONFIG.y('passbook.uid_fields'): + for search_field in CONFIG.y("passbook.uid_fields"): # Workaround for E-Mail -> email - if search_field == 'e-mail': - search_field = 'email' + if search_field == "e-mail": + search_field = "email" users = User.objects.filter(**{search_field: uid_value}) if users.exists(): LOGGER.debug("Found user", user=users.first(), uid_field=search_field) @@ -68,18 +68,20 @@ class LoginView(UserPassesTestMixin, FormView): def form_valid(self, form: LoginForm) -> HttpResponse: """Form data is valid""" - pre_user = self.get_user(form.cleaned_data.get('uid_field')) + pre_user = self.get_user(form.cleaned_data.get("uid_field")) if not pre_user: # No user found return self.invalid_login(self.request) # self.request.session.flush() self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk - return _redirect_with_qs('passbook_core:auth-process', self.request.GET) + return _redirect_with_qs("passbook_core:auth-process", self.request.GET) - def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse: + def invalid_login( + self, request: HttpRequest, disabled_user: User = None + ) -> HttpResponse: """Handle login for disabled users/invalid login attempts""" LOGGER.debug("invalid_login", user=disabled_user) - messages.error(request, _('Failed to authenticate.')) + messages.error(request, _("Failed to authenticate.")) return self.render_to_response(self.get_context_data()) @@ -90,15 +92,15 @@ class LogoutView(LoginRequiredMixin, View): """Log current user out""" logout(request) messages.success(request, _("You've successfully been logged out.")) - return redirect(reverse('passbook_core:auth-login')) + return redirect(reverse("passbook_core:auth-login")) class SignUpView(UserPassesTestMixin, FormView): """Sign up new user, optionally consume one-use invitation link.""" - template_name = 'login/form.html' + template_name = "login/form.html" form_class = SignUpForm - success_url = '.' + success_url = "." # Invitation instance, if invitation link was used _invitation = None # Instance of newly created user @@ -109,38 +111,38 @@ class SignUpView(UserPassesTestMixin, FormView): return self.request.user.is_authenticated is False def handle_no_permission(self): - return redirect(reverse('passbook_core:overview')) + return redirect(reverse("passbook_core:overview")) def dispatch(self, request, *args, **kwargs): """Check if sign-up is enabled or invitation link given""" allowed = False - if 'invitation' in request.GET: - invitations = Invitation.objects.filter(uuid=request.GET.get('invitation')) + if "invitation" in request.GET: + invitations = Invitation.objects.filter(uuid=request.GET.get("invitation")) allowed = invitations.exists() if allowed: self._invitation = invitations.first() - if CONFIG.y('passbook.sign_up.enabled'): + if CONFIG.y("passbook.sign_up.enabled"): allowed = True if not allowed: - messages.error(request, _('Sign-ups are currently disabled.')) - return redirect(reverse('passbook_core:auth-login')) + messages.error(request, _("Sign-ups are currently disabled.")) + return redirect(reverse("passbook_core:auth-login")) return super().dispatch(request, *args, **kwargs) def get_initial(self): if self._invitation: initial = {} if self._invitation.fixed_username: - initial['username'] = self._invitation.fixed_username + initial["username"] = self._invitation.fixed_username if self._invitation.fixed_email: - initial['email'] = self._invitation.fixed_email + initial["email"] = self._invitation.fixed_email return initial return super().get_initial() def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.y('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Sign Up') - kwargs['primary_action'] = _('Sign up') + kwargs["config"] = CONFIG.y("passbook") + kwargs["is_login"] = True + kwargs["title"] = _("Sign Up") + kwargs["primary_action"] = _("Sign up") return super().get_context_data(**kwargs) def form_valid(self, form: SignUpForm) -> HttpResponse: @@ -173,9 +175,8 @@ class SignUpView(UserPassesTestMixin, FormView): # self._user.save() self.consume_invitation() messages.success(self.request, _("Successfully signed up!")) - LOGGER.debug("Successfully signed up %s", - form.cleaned_data.get('email')) - return redirect(reverse('passbook_core:auth-login')) + LOGGER.debug("Successfully signed up %s", form.cleaned_data.get("email")) + return redirect(reverse("passbook_core:auth-login")) def consume_invitation(self): """Consume invitation if an invitation was used""" @@ -184,7 +185,8 @@ class SignUpView(UserPassesTestMixin, FormView): sender=self, request=self.request, invitation=self._invitation, - user=self._user) + user=self._user, + ) self._invitation.delete() @staticmethod @@ -204,20 +206,17 @@ class SignUpView(UserPassesTestMixin, FormView): """ # Create user new_user = User.objects.create( - username=data.get('username'), - email=data.get('email'), - name=data.get('name'), + username=data.get("username"), + email=data.get("email"), + name=data.get("name"), ) new_user.is_active = True try: - new_user.set_password(data.get('password')) + new_user.set_password(data.get("password")) new_user.save() request.user = new_user # Send signal for other auth sources - user_signed_up.send( - sender=SignUpView, - user=new_user, - request=request) + user_signed_up.send(sender=SignUpView, user=new_user, request=request) return new_user except PasswordPolicyInvalid as exc: new_user.delete() @@ -233,11 +232,11 @@ class SignUpConfirmView(View): nonce.user.is_active = True nonce.user.save() # Workaround: hardcoded reference to ModelBackend, needs testing - nonce.user.backend = 'django.contrib.auth.backends.ModelBackend' + nonce.user.backend = "django.contrib.auth.backends.ModelBackend" login(request, nonce.user) nonce.delete() - messages.success(request, _('Successfully confirmed registration.')) - return redirect('passbook_core:overview') + messages.success(request, _("Successfully confirmed registration.")) + return redirect("passbook_core:overview") class PasswordResetView(View): @@ -248,9 +247,11 @@ class PasswordResetView(View): # 3. (Optional) Trap user in password change view nonce = get_object_or_404(Nonce, uuid=nonce) # Workaround: hardcoded reference to ModelBackend, needs testing - nonce.user.backend = 'django.contrib.auth.backends.ModelBackend' + nonce.user.backend = "django.contrib.auth.backends.ModelBackend" login(request, nonce.user) nonce.delete() - messages.success(request, _(('Temporarily authenticated with Nonce, ' - 'please change your password'))) - return redirect('passbook_core:user-change-password') + messages.success( + request, + _(("Temporarily authenticated with Nonce, " "please change your password")), + ) + return redirect("passbook_core:user-change-password") diff --git a/passbook/core/views/error.py b/passbook/core/views/error.py index f2ec1b337..b9301d385 100644 --- a/passbook/core/views/error.py +++ b/passbook/core/views/error.py @@ -1,8 +1,11 @@ """passbook core error views""" -from django.http.response import (HttpResponseBadRequest, - HttpResponseForbidden, HttpResponseNotFound, - HttpResponseServerError) +from django.http.response import ( + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseServerError, +) from django.template.response import TemplateResponse from django.views.generic import TemplateView @@ -10,54 +13,53 @@ from django.views.generic import TemplateView class BadRequestTemplateResponse(TemplateResponse, HttpResponseBadRequest): """Combine Template response with Http Code 400""" + class ForbiddenTemplateResponse(TemplateResponse, HttpResponseForbidden): """Combine Template response with Http Code 403""" + class NotFoundTemplateResponse(TemplateResponse, HttpResponseNotFound): """Combine Template response with Http Code 404""" + class ServerErrorTemplateResponse(TemplateResponse, HttpResponseServerError): """Combine Template response with Http Code 500""" + class BadRequestView(TemplateView): """Show Bad Request message""" response_class = BadRequestTemplateResponse - template_name = 'error/400.html' + template_name = "error/400.html" + + extra_context = {"is_login": True} - extra_context = { - 'is_login': True - } class ForbiddenView(TemplateView): """Show Forbidden message""" response_class = ForbiddenTemplateResponse - template_name = 'error/403.html' + template_name = "error/403.html" + + extra_context = {"is_login": True} - extra_context = { - 'is_login': True - } class NotFoundView(TemplateView): """Show Not Found message""" response_class = NotFoundTemplateResponse - template_name = 'error/404.html' + template_name = "error/404.html" + + extra_context = {"is_login": True} - extra_context = { - 'is_login': True - } class ServerErrorView(TemplateView): """Show Server Error message""" response_class = ServerErrorTemplateResponse - template_name = 'error/500.html' + template_name = "error/500.html" - extra_context = { - 'is_login': True - } + extra_context = {"is_login": True} # pylint: disable=useless-super-delegation def dispatch(self, *args, **kwargs): diff --git a/passbook/core/views/overview.py b/passbook/core/views/overview.py index b1c62d25a..e7ad3f657 100644 --- a/passbook/core/views/overview.py +++ b/passbook/core/views/overview.py @@ -11,13 +11,15 @@ class OverviewView(LoginRequiredMixin, TemplateView): """Overview for logged in user, incase user opens passbook directly and is not being forwarded""" - template_name = 'overview/index.html' + template_name = "overview/index.html" def get_context_data(self, **kwargs): - kwargs['applications'] = [] + kwargs["applications"] = [] for application in Application.objects.all(): - engine = PolicyEngine(application.policies.all(), self.request.user, self.request) + engine = PolicyEngine( + application.policies.all(), self.request.user, self.request + ) engine.build() if engine.passing: - kwargs['applications'].append(application) + kwargs["applications"].append(application) return super().get_context_data(**kwargs) diff --git a/passbook/core/views/user.py b/passbook/core/views/user.py index 4f90a1421..caa4e3914 100644 --- a/passbook/core/views/user.py +++ b/passbook/core/views/user.py @@ -17,11 +17,11 @@ from passbook.lib.config import CONFIG class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): """Update User settings""" - template_name = 'user/settings.html' + template_name = "user/settings.html" form_class = UserDetailForm - success_message = _('Successfully updated user.') - success_url = reverse_lazy('passbook_core:user-settings') + success_message = _("Successfully updated user.") + success_url = reverse_lazy("passbook_core:user-settings") def get_object(self): return self.request.user @@ -30,44 +30,44 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): class UserDeleteView(LoginRequiredMixin, DeleteView): """Delete user account""" - template_name = 'generic/delete.html' + template_name = "generic/delete.html" def get_object(self): return self.request.user def get_success_url(self): - messages.success(self.request, _('Successfully deleted user.')) + messages.success(self.request, _("Successfully deleted user.")) logout(self.request) - return reverse('passbook_core:auth-login') + return reverse("passbook_core:auth-login") class UserChangePasswordView(LoginRequiredMixin, FormView): """View for users to update their password""" form_class = PasswordChangeForm - template_name = 'login/form_with_user.html' + template_name = "login/form_with_user.html" def form_valid(self, form: PasswordChangeForm): try: # user.set_password checks against Policies so we don't need to manually do it here - self.request.user.set_password(form.cleaned_data.get('password')) + self.request.user.set_password(form.cleaned_data.get("password")) self.request.user.save() update_session_auth_hash(self.request, self.request.user) - messages.success(self.request, _('Successfully changed password')) + messages.success(self.request, _("Successfully changed password")) except PasswordPolicyInvalid as exc: # Manually inject error into form # pylint: disable=protected-access - errors = form._errors.setdefault("password_repeat", ErrorList('')) + errors = form._errors.setdefault("password_repeat", ErrorList("")) # pylint: disable=protected-access errors = form._errors.setdefault("password", ErrorList()) for error in exc.messages: errors.append(error) return self.form_invalid(form) - return redirect('passbook_core:overview') + return redirect("passbook_core:overview") def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.y('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Change Password') - kwargs['primary_action'] = _('Change') + kwargs["config"] = CONFIG.y("passbook") + kwargs["is_login"] = True + kwargs["title"] = _("Change Password") + kwargs["primary_action"] = _("Change") return super().get_context_data(**kwargs) diff --git a/passbook/core/views/utils.py b/passbook/core/views/utils.py index d73199cee..1bedcff24 100644 --- a/passbook/core/views/utils.py +++ b/passbook/core/views/utils.py @@ -6,8 +6,8 @@ from django.views.generic import TemplateView class LoadingView(TemplateView): """View showing a loading template, and forwarding to real view using html forwarding.""" - template_name = 'login/loading.html' - title = _('Loading') + template_name = "login/loading.html" + title = _("Loading") target_url = None def get_url(self): @@ -15,18 +15,19 @@ class LoadingView(TemplateView): return self.target_url def get_context_data(self, **kwargs): - kwargs['is_login'] = True - kwargs['title'] = self.title - kwargs['target_url'] = self.get_url() + kwargs["is_login"] = True + kwargs["title"] = self.title + kwargs["target_url"] = self.get_url() return super().get_context_data(**kwargs) + class PermissionDeniedView(TemplateView): """Generic Permission denied view""" - template_name = 'login/denied.html' - title = _('Permission denied.') + template_name = "login/denied.html" + title = _("Permission denied.") def get_context_data(self, **kwargs): - kwargs['is_login'] = True - kwargs['title'] = self.title + kwargs["is_login"] = True + kwargs["title"] = self.title return super().get_context_data(**kwargs) diff --git a/passbook/factors/base.py b/passbook/factors/base.py index e3219f7b3..1aec40ca5 100644 --- a/passbook/factors/base.py +++ b/passbook/factors/base.py @@ -17,16 +17,16 @@ class AuthenticationFactor(TemplateView): authenticator: AuthenticationView pending_user: User request: HttpRequest = None - template_name = 'login/form_with_user.html' + template_name = "login/form_with_user.html" def __init__(self, authenticator: AuthenticationView): self.authenticator = authenticator self.pending_user = None def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.y('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Log in to your account') - kwargs['primary_action'] = _('Log in') - kwargs['user'] = self.pending_user + kwargs["config"] = CONFIG.y("passbook") + kwargs["is_login"] = True + kwargs["title"] = _("Log in to your account") + kwargs["primary_action"] = _("Log in") + kwargs["user"] = self.pending_user return super().get_context_data(**kwargs) diff --git a/passbook/factors/captcha/admin.py b/passbook/factors/captcha/admin.py index 50049a8d0..88f03befa 100644 --- a/passbook/factors/captcha/admin.py +++ b/passbook/factors/captcha/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_factors_captcha') +admin_autoregister("passbook_factors_captcha") diff --git a/passbook/factors/captcha/api.py b/passbook/factors/captcha/api.py index ab8d5fb47..0786321b4 100644 --- a/passbook/factors/captcha/api.py +++ b/passbook/factors/captcha/api.py @@ -11,7 +11,7 @@ class CaptchaFactorSerializer(ModelSerializer): class Meta: model = CaptchaFactor - fields = ['pk', 'name', 'slug', 'order', 'enabled', 'public_key', 'private_key'] + fields = ["pk", "name", "slug", "order", "enabled", "public_key", "private_key"] class CaptchaFactorViewSet(ModelViewSet): diff --git a/passbook/factors/captcha/apps.py b/passbook/factors/captcha/apps.py index 5b4e0c722..d054894ae 100644 --- a/passbook/factors/captcha/apps.py +++ b/passbook/factors/captcha/apps.py @@ -5,6 +5,6 @@ from django.apps import AppConfig class PassbookFactorCaptchaConfig(AppConfig): """passbook captcha app""" - name = 'passbook.factors.captcha' - label = 'passbook_factors_captcha' - verbose_name = 'passbook Factors.Captcha' + name = "passbook.factors.captcha" + label = "passbook_factors_captcha" + verbose_name = "passbook Factors.Captcha" diff --git a/passbook/factors/captcha/factor.py b/passbook/factors/captcha/factor.py index b5f9df5fb..88fdd4c15 100644 --- a/passbook/factors/captcha/factor.py +++ b/passbook/factors/captcha/factor.py @@ -16,7 +16,11 @@ class CaptchaFactor(FormView, AuthenticationFactor): def get_form(self, form_class=None): form = CaptchaForm(**self.get_form_kwargs()) - form.fields['captcha'].public_key = self.authenticator.current_factor.public_key - form.fields['captcha'].private_key = self.authenticator.current_factor.private_key - form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key + form.fields["captcha"].public_key = self.authenticator.current_factor.public_key + form.fields[ + "captcha" + ].private_key = self.authenticator.current_factor.private_key + form.fields["captcha"].widget.attrs["data-sitekey"] = form.fields[ + "captcha" + ].public_key return form diff --git a/passbook/factors/captcha/forms.py b/passbook/factors/captcha/forms.py index fdbdeca7f..9084b6462 100644 --- a/passbook/factors/captcha/forms.py +++ b/passbook/factors/captcha/forms.py @@ -13,17 +13,18 @@ class CaptchaForm(forms.Form): captcha = ReCaptchaField() + class CaptchaFactorForm(forms.ModelForm): """Form to edit CaptchaFactor Instance""" class Meta: model = CaptchaFactor - fields = GENERAL_FIELDS + ['public_key', 'private_key'] + fields = GENERAL_FIELDS + ["public_key", "private_key"] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False), - 'public_key': forms.TextInput(), - 'private_key': forms.TextInput(), + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), + "public_key": forms.TextInput(), + "private_key": forms.TextInput(), } diff --git a/passbook/factors/captcha/migrations/0001_initial.py b/passbook/factors/captcha/migrations/0001_initial.py index e08a8efdd..0f44952d8 100644 --- a/passbook/factors/captcha/migrations/0001_initial.py +++ b/passbook/factors/captcha/migrations/0001_initial.py @@ -9,21 +9,31 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='CaptchaFactor', + name="CaptchaFactor", fields=[ - ('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')), - ('public_key', models.TextField()), - ('private_key', models.TextField()), + ( + "factor_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Factor", + ), + ), + ("public_key", models.TextField()), + ("private_key", models.TextField()), ], options={ - 'verbose_name': 'Captcha Factor', - 'verbose_name_plural': 'Captcha Factors', + "verbose_name": "Captcha Factor", + "verbose_name_plural": "Captcha Factors", }, - bases=('passbook_core.factor',), + bases=("passbook_core.factor",), ), ] diff --git a/passbook/factors/captcha/models.py b/passbook/factors/captcha/models.py index 9b45bbb57..84d96c59c 100644 --- a/passbook/factors/captcha/models.py +++ b/passbook/factors/captcha/models.py @@ -11,13 +11,13 @@ class CaptchaFactor(Factor): public_key = models.TextField() private_key = models.TextField() - type = 'passbook.factors.captcha.factor.CaptchaFactor' - form = 'passbook.factors.captcha.forms.CaptchaFactorForm' + type = "passbook.factors.captcha.factor.CaptchaFactor" + form = "passbook.factors.captcha.forms.CaptchaFactorForm" def __str__(self): return f"Captcha Factor {self.slug}" class Meta: - verbose_name = _('Captcha Factor') - verbose_name_plural = _('Captcha Factors') + verbose_name = _("Captcha Factor") + verbose_name_plural = _("Captcha Factors") diff --git a/passbook/factors/captcha/settings.py b/passbook/factors/captcha/settings.py index 01e98b632..7466ad4f8 100644 --- a/passbook/factors/captcha/settings.py +++ b/passbook/factors/captcha/settings.py @@ -1,10 +1,8 @@ """passbook captcha_factor settings""" # https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do -RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' -RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe' +RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" +RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" NOCAPTCHA = True -INSTALLED_APPS = [ - 'captcha' -] -SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] +INSTALLED_APPS = ["captcha"] +SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] diff --git a/passbook/factors/dummy/admin.py b/passbook/factors/dummy/admin.py index 0c3f336fc..2499671fe 100644 --- a/passbook/factors/dummy/admin.py +++ b/passbook/factors/dummy/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_factors_dummy') +admin_autoregister("passbook_factors_dummy") diff --git a/passbook/factors/dummy/api.py b/passbook/factors/dummy/api.py index 96f9c0beb..108698aaa 100644 --- a/passbook/factors/dummy/api.py +++ b/passbook/factors/dummy/api.py @@ -11,7 +11,7 @@ class DummyFactorSerializer(ModelSerializer): class Meta: model = DummyFactor - fields = ['pk', 'name', 'slug', 'order', 'enabled'] + fields = ["pk", "name", "slug", "order", "enabled"] class DummyFactorViewSet(ModelViewSet): diff --git a/passbook/factors/dummy/apps.py b/passbook/factors/dummy/apps.py index 2a5eb82c3..4cb858b88 100644 --- a/passbook/factors/dummy/apps.py +++ b/passbook/factors/dummy/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookFactorDummyConfig(AppConfig): """passbook dummy factor config""" - name = 'passbook.factors.dummy' - label = 'passbook_factors_dummy' - verbose_name = 'passbook Factors.Dummy' + name = "passbook.factors.dummy" + label = "passbook_factors_dummy" + verbose_name = "passbook Factors.Dummy" diff --git a/passbook/factors/dummy/forms.py b/passbook/factors/dummy/forms.py index cf5a9145e..c3a0bdf7f 100644 --- a/passbook/factors/dummy/forms.py +++ b/passbook/factors/dummy/forms.py @@ -15,7 +15,7 @@ class DummyFactorForm(forms.ModelForm): model = DummyFactor fields = GENERAL_FIELDS widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), } diff --git a/passbook/factors/dummy/migrations/0001_initial.py b/passbook/factors/dummy/migrations/0001_initial.py index 59abab393..d0a905a87 100644 --- a/passbook/factors/dummy/migrations/0001_initial.py +++ b/passbook/factors/dummy/migrations/0001_initial.py @@ -9,19 +9,29 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='DummyFactor', + name="DummyFactor", fields=[ - ('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')), + ( + "factor_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Factor", + ), + ), ], options={ - 'verbose_name': 'Dummy Factor', - 'verbose_name_plural': 'Dummy Factors', + "verbose_name": "Dummy Factor", + "verbose_name_plural": "Dummy Factors", }, - bases=('passbook_core.factor',), + bases=("passbook_core.factor",), ), ] diff --git a/passbook/factors/dummy/models.py b/passbook/factors/dummy/models.py index bbf4c6b36..a1e24d6ab 100644 --- a/passbook/factors/dummy/models.py +++ b/passbook/factors/dummy/models.py @@ -7,13 +7,13 @@ from passbook.core.models import Factor class DummyFactor(Factor): """Dummy factor, mostly used to debug""" - type = 'passbook.factors.dummy.factor.DummyFactor' - form = 'passbook.factors.dummy.forms.DummyFactorForm' + type = "passbook.factors.dummy.factor.DummyFactor" + form = "passbook.factors.dummy.forms.DummyFactorForm" def __str__(self): return f"Dummy Factor {self.slug}" class Meta: - verbose_name = _('Dummy Factor') - verbose_name_plural = _('Dummy Factors') + verbose_name = _("Dummy Factor") + verbose_name_plural = _("Dummy Factors") diff --git a/passbook/factors/email/admin.py b/passbook/factors/email/admin.py index 305cc3acd..e5343ef52 100644 --- a/passbook/factors/email/admin.py +++ b/passbook/factors/email/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_factors_email') +admin_autoregister("passbook_factors_email") diff --git a/passbook/factors/email/api.py b/passbook/factors/email/api.py index b2b3b8372..165ce9064 100644 --- a/passbook/factors/email/api.py +++ b/passbook/factors/email/api.py @@ -11,19 +11,24 @@ class EmailFactorSerializer(ModelSerializer): class Meta: model = EmailFactor - fields = ['pk', 'name', 'slug', 'order', 'enabled', 'host', - 'port', - 'username', - 'password', - 'use_tls', - 'use_ssl', - 'timeout', - 'from_address', - 'ssl_keyfile', - 'ssl_certfile', ] - extra_kwargs = { - 'password': {'write_only': True} - } + fields = [ + "pk", + "name", + "slug", + "order", + "enabled", + "host", + "port", + "username", + "password", + "use_tls", + "use_ssl", + "timeout", + "from_address", + "ssl_keyfile", + "ssl_certfile", + ] + extra_kwargs = {"password": {"write_only": True}} class EmailFactorViewSet(ModelViewSet): diff --git a/passbook/factors/email/apps.py b/passbook/factors/email/apps.py index f6f04c9bc..1382cf173 100644 --- a/passbook/factors/email/apps.py +++ b/passbook/factors/email/apps.py @@ -7,9 +7,9 @@ from django.apps import AppConfig class PassbookFactorEmailConfig(AppConfig): """passbook email factor config""" - name = 'passbook.factors.email' - label = 'passbook_factors_email' - verbose_name = 'passbook Factors.Email' + name = "passbook.factors.email" + label = "passbook_factors_email" + verbose_name = "passbook Factors.Email" def ready(self): - import_module('passbook.factors.email.tasks') + import_module("passbook.factors.email.tasks") diff --git a/passbook/factors/email/factor.py b/passbook/factors/email/factor.py index dbd70e0e6..082d7ef8a 100644 --- a/passbook/factors/email/factor.py +++ b/passbook/factors/email/factor.py @@ -18,27 +18,31 @@ class EmailFactorView(AuthenticationFactor): """Dummy factor for testing with multiple factors""" def get_context_data(self, **kwargs): - kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled') + kwargs["show_password_forget_notice"] = CONFIG.y( + "passbook.password_reset.enabled" + ) return super().get_context_data(**kwargs) def get(self, request, *args, **kwargs): nonce = Nonce.objects.create(user=self.pending_user) # Send mail to user message = TemplateEmailMessage( - subject=_('Forgotten password'), - template_name='email/account_password_reset.html', + subject=_("Forgotten password"), + template_name="email/account_password_reset.html", to=[self.pending_user.email], template_context={ - 'url': self.request.build_absolute_uri( - reverse('passbook_core:auth-password-reset', - kwargs={ - 'nonce': nonce.uuid - }) - )}) + "url": self.request.build_absolute_uri( + reverse( + "passbook_core:auth-password-reset", + kwargs={"nonce": nonce.uuid}, + ) + ) + }, + ) send_mails(self.authenticator.current_factor, message) self.authenticator.cleanup() - messages.success(request, _('Check your E-Mails for a password reset link.')) - return redirect('passbook_core:auth-login') + messages.success(request, _("Check your E-Mails for a password reset link.")) + return redirect("passbook_core:auth-login") def post(self, request: HttpRequest): """Just redirect to next factor""" diff --git a/passbook/factors/email/forms.py b/passbook/factors/email/forms.py index 030f2efaa..5793ea864 100644 --- a/passbook/factors/email/forms.py +++ b/passbook/factors/email/forms.py @@ -14,30 +14,30 @@ class EmailFactorForm(forms.ModelForm): model = EmailFactor fields = GENERAL_FIELDS + [ - 'host', - 'port', - 'username', - 'password', - 'use_tls', - 'use_ssl', - 'timeout', - 'from_address', - 'ssl_keyfile', - 'ssl_certfile', + "host", + "port", + "username", + "password", + "use_tls", + "use_ssl", + "timeout", + "from_address", + "ssl_keyfile", + "ssl_certfile", ] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False), - 'host': forms.TextInput(), - 'username': forms.TextInput(), - 'password': forms.TextInput(), - 'ssl_keyfile': forms.TextInput(), - 'ssl_certfile': forms.TextInput(), + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), + "host": forms.TextInput(), + "username": forms.TextInput(), + "password": forms.TextInput(), + "ssl_keyfile": forms.TextInput(), + "ssl_certfile": forms.TextInput(), } labels = { - 'use_tls': _('Use TLS'), - 'use_ssl': _('Use SSL'), - 'ssl_keyfile': _('SSL Keyfile (optional)'), - 'ssl_certfile': _('SSL Certfile (optional)'), + "use_tls": _("Use TLS"), + "use_ssl": _("Use SSL"), + "ssl_keyfile": _("SSL Keyfile (optional)"), + "ssl_certfile": _("SSL Certfile (optional)"), } diff --git a/passbook/factors/email/migrations/0001_initial.py b/passbook/factors/email/migrations/0001_initial.py index 68d5a99cd..e9b8519b2 100644 --- a/passbook/factors/email/migrations/0001_initial.py +++ b/passbook/factors/email/migrations/0001_initial.py @@ -9,29 +9,42 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='EmailFactor', + name="EmailFactor", fields=[ - ('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')), - ('host', models.TextField(default='localhost')), - ('port', models.IntegerField(default=25)), - ('username', models.TextField(blank=True, default='')), - ('password', models.TextField(blank=True, default='')), - ('use_tls', models.BooleanField(default=False)), - ('use_ssl', models.BooleanField(default=False)), - ('timeout', models.IntegerField(default=0)), - ('ssl_keyfile', models.TextField(blank=True, default=None, null=True)), - ('ssl_certfile', models.TextField(blank=True, default=None, null=True)), - ('from_address', models.EmailField(default='system@passbook.local', max_length=254)), + ( + "factor_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Factor", + ), + ), + ("host", models.TextField(default="localhost")), + ("port", models.IntegerField(default=25)), + ("username", models.TextField(blank=True, default="")), + ("password", models.TextField(blank=True, default="")), + ("use_tls", models.BooleanField(default=False)), + ("use_ssl", models.BooleanField(default=False)), + ("timeout", models.IntegerField(default=0)), + ("ssl_keyfile", models.TextField(blank=True, default=None, null=True)), + ("ssl_certfile", models.TextField(blank=True, default=None, null=True)), + ( + "from_address", + models.EmailField(default="system@passbook.local", max_length=254), + ), ], options={ - 'verbose_name': 'Email Factor', - 'verbose_name_plural': 'Email Factors', + "verbose_name": "Email Factor", + "verbose_name_plural": "Email Factors", }, - bases=('passbook_core.factor',), + bases=("passbook_core.factor",), ), ] diff --git a/passbook/factors/email/migrations/0002_auto_20191011_1224.py b/passbook/factors/email/migrations/0002_auto_20191011_1224.py index 7b2e96cc3..b020e7c5a 100644 --- a/passbook/factors/email/migrations/0002_auto_20191011_1224.py +++ b/passbook/factors/email/migrations/0002_auto_20191011_1224.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_factors_email', '0001_initial'), + ("passbook_factors_email", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='emailfactor', - name='timeout', + model_name="emailfactor", + name="timeout", field=models.IntegerField(default=10), ), ] diff --git a/passbook/factors/email/models.py b/passbook/factors/email/models.py index c02a257dd..f2027c1d9 100644 --- a/passbook/factors/email/models.py +++ b/passbook/factors/email/models.py @@ -9,10 +9,10 @@ from passbook.core.models import Factor class EmailFactor(Factor): """email factor""" - host = models.TextField(default='localhost') + host = models.TextField(default="localhost") port = models.IntegerField(default=25) - username = models.TextField(default='', blank=True) - password = models.TextField(default='', blank=True) + username = models.TextField(default="", blank=True) + password = models.TextField(default="", blank=True) use_tls = models.BooleanField(default=False) use_ssl = models.BooleanField(default=False) timeout = models.IntegerField(default=10) @@ -20,10 +20,10 @@ class EmailFactor(Factor): ssl_keyfile = models.TextField(default=None, blank=True, null=True) ssl_certfile = models.TextField(default=None, blank=True, null=True) - from_address = models.EmailField(default='system@passbook.local') + from_address = models.EmailField(default="system@passbook.local") - type = 'passbook.factors.email.factor.EmailFactorView' - form = 'passbook.factors.email.forms.EmailFactorForm' + type = "passbook.factors.email.factor.EmailFactorView" + form = "passbook.factors.email.forms.EmailFactorForm" @property def backend(self) -> EmailBackend: @@ -37,12 +37,13 @@ class EmailFactor(Factor): use_ssl=self.use_ssl, timeout=self.timeout, ssl_certfile=self.ssl_certfile, - ssl_keyfile=self.ssl_keyfile) + ssl_keyfile=self.ssl_keyfile, + ) def __str__(self): return f"Email Factor {self.slug}" class Meta: - verbose_name = _('Email Factor') - verbose_name_plural = _('Email Factors') + verbose_name = _("Email Factor") + verbose_name_plural = _("Email Factors") diff --git a/passbook/factors/email/tasks.py b/passbook/factors/email/tasks.py index 5ec02de79..b3e752b04 100644 --- a/passbook/factors/email/tasks.py +++ b/passbook/factors/email/tasks.py @@ -21,6 +21,7 @@ def send_mails(factor: EmailFactor, *messages: List[EmailMessage]): promise = lazy_group() return promise + @CELERY_APP.task(bind=True) def _send_mail_task(self, email_factor_pk: int, message: Dict[Any, Any]): """Send E-Mail according to EmailFactor parameters from background worker. diff --git a/passbook/factors/email/utils.py b/passbook/factors/email/utils.py index 3816df29d..a94f4c5d6 100644 --- a/passbook/factors/email/utils.py +++ b/passbook/factors/email/utils.py @@ -8,9 +8,21 @@ class TemplateEmailMessage(EmailMultiAlternatives): """Wrapper around EmailMultiAlternatives with integrated template rendering""" # pylint: disable=too-many-arguments - def __init__(self, subject='', body=None, from_email=None, to=None, bcc=None, - connection=None, attachments=None, headers=None, cc=None, - reply_to=None, template_name=None, template_context=None): + def __init__( + self, + subject="", + body=None, + from_email=None, + to=None, + bcc=None, + connection=None, + attachments=None, + headers=None, + cc=None, + reply_to=None, + template_name=None, + template_context=None, + ): html_content = render_to_string(template_name, template_context) if not body: body = strip_tags(html_content) @@ -24,5 +36,6 @@ class TemplateEmailMessage(EmailMultiAlternatives): attachments=attachments, headers=headers, cc=cc, - reply_to=reply_to) + reply_to=reply_to, + ) self.attach_alternative(html_content, "text/html") diff --git a/passbook/factors/forms.py b/passbook/factors/forms.py index 1f65394ac..6bc2443bb 100644 --- a/passbook/factors/forms.py +++ b/passbook/factors/forms.py @@ -1,3 +1,3 @@ """factor forms""" -GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled'] +GENERAL_FIELDS = ["name", "slug", "order", "policies", "enabled"] diff --git a/passbook/factors/otp/api.py b/passbook/factors/otp/api.py index 437da1b91..4962ddc46 100644 --- a/passbook/factors/otp/api.py +++ b/passbook/factors/otp/api.py @@ -11,7 +11,7 @@ class OTPFactorSerializer(ModelSerializer): class Meta: model = OTPFactor - fields = ['pk', 'name', 'slug', 'order', 'enabled', 'enforced'] + fields = ["pk", "name", "slug", "order", "enabled", "enforced"] class OTPFactorViewSet(ModelViewSet): diff --git a/passbook/factors/otp/apps.py b/passbook/factors/otp/apps.py index 1fc1be8e6..d04263248 100644 --- a/passbook/factors/otp/apps.py +++ b/passbook/factors/otp/apps.py @@ -6,7 +6,7 @@ from django.apps.config import AppConfig class PassbookFactorOTPConfig(AppConfig): """passbook OTP AppConfig""" - name = 'passbook.factors.otp' - label = 'passbook_factors_otp' - verbose_name = 'passbook Factors.OTP' - mountpoint = 'user/otp/' + name = "passbook.factors.otp" + label = "passbook_factors_otp" + verbose_name = "passbook Factors.OTP" + mountpoint = "user/otp/" diff --git a/passbook/factors/otp/factors.py b/passbook/factors/otp/factors.py index 9cae91761..e16d8111b 100644 --- a/passbook/factors/otp/factors.py +++ b/passbook/factors/otp/factors.py @@ -11,15 +11,16 @@ from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView LOGGER = get_logger() + class OTPFactor(FormView, AuthenticationFactor): """OTP Factor View""" - template_name = 'otp/factor.html' + template_name = "otp/factor.html" form_class = OTPVerifyForm def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - kwargs['title'] = _('Enter Verification Code') + kwargs["title"] = _("Enter Verification Code") return kwargs def get(self, request, *args, **kwargs): @@ -31,7 +32,7 @@ class OTPFactor(FormView, AuthenticationFactor): LOGGER.debug("OTP is enforced, redirecting to setup") request.user = self.pending_user LOGGER.debug("Passing GET to EnableView") - messages.info(request, _('OTP is enforced. Please setup OTP.')) + messages.info(request, _("OTP is enforced. Please setup OTP.")) return EnableView.as_view()(request) LOGGER.debug("OTP is not enforced, skipping form") return self.authenticator.user_ok() @@ -47,8 +48,8 @@ class OTPFactor(FormView, AuthenticationFactor): def form_valid(self, form: OTPVerifyForm): """Verify OTP Token""" - device = match_token(self.pending_user, form.cleaned_data.get('code')) + device = match_token(self.pending_user, form.cleaned_data.get("code")) if device: return self.authenticator.user_ok() - messages.error(self.request, _('Invalid OTP.')) + messages.error(self.request, _("Invalid OTP.")) return self.form_invalid(form) diff --git a/passbook/factors/otp/forms.py b/passbook/factors/otp/forms.py index fdc129d28..4b665b991 100644 --- a/passbook/factors/otp/forms.py +++ b/passbook/factors/otp/forms.py @@ -10,53 +10,63 @@ from django_otp.models import Device from passbook.factors.forms import GENERAL_FIELDS from passbook.factors.otp.models import OTPFactor -OTP_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$', - _('Only alpha-numeric characters are allowed.')) +OTP_CODE_VALIDATOR = RegexValidator( + r"^[0-9a-z]{6,8}$", _("Only alpha-numeric characters are allowed.") +) class PictureWidget(forms.widgets.Widget): """Widget to render value as img-tag""" def render(self, name, value, attrs=None, renderer=None): - return mark_safe(f'') # nosec + return mark_safe(f'') # nosec class OTPVerifyForm(forms.Form): """Simple Form to verify OTP Code""" - order = ['code'] - code = forms.CharField(label=_('Code'), validators=[OTP_CODE_VALIDATOR], - widget=forms.TextInput(attrs={ - 'autocomplete': 'off', - 'placeholder': 'Code' - })) + order = ["code"] + + code = forms.CharField( + label=_("Code"), + validators=[OTP_CODE_VALIDATOR], + widget=forms.TextInput(attrs={"autocomplete": "off", "placeholder": "Code"}), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # This is a little helper so the field is focused by default - self.fields['code'].widget.attrs.update({ - 'autofocus': 'autofocus', - 'autocomplete': 'off' - }) + self.fields["code"].widget.attrs.update( + {"autofocus": "autofocus", "autocomplete": "off"} + ) class OTPSetupForm(forms.Form): """OTP Setup form""" - title = _('Set up OTP') + + title = _("Set up OTP") device: Device = None - qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False, - label=_('Scan this Code with your OTP App.')) - code = forms.CharField(label=_('Code'), validators=[OTP_CODE_VALIDATOR], - widget=forms.TextInput(attrs={'placeholder': _('One-Time Password')})) + qr_code = forms.CharField( + widget=PictureWidget, + disabled=True, + required=False, + label=_("Scan this Code with your OTP App."), + ) + code = forms.CharField( + label=_("Code"), + validators=[OTP_CODE_VALIDATOR], + widget=forms.TextInput(attrs={"placeholder": _("One-Time Password")}), + ) tokens = forms.MultipleChoiceField(disabled=True, required=False) def clean_code(self): """Check code with new otp device""" if self.device is not None: - if not self.device.verify_token(int(self.cleaned_data.get('code'))): + if not self.device.verify_token(int(self.cleaned_data.get("code"))): raise forms.ValidationError(_("OTP Code does not match")) - return self.cleaned_data.get('code') + return self.cleaned_data.get("code") + class OTPFactorForm(forms.ModelForm): """Form to edit OTPFactor instances""" @@ -64,9 +74,9 @@ class OTPFactorForm(forms.ModelForm): class Meta: model = OTPFactor - fields = GENERAL_FIELDS + ['enforced'] + fields = GENERAL_FIELDS + ["enforced"] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), } diff --git a/passbook/factors/otp/migrations/0001_initial.py b/passbook/factors/otp/migrations/0001_initial.py index 0acab2c9d..fe06e9300 100644 --- a/passbook/factors/otp/migrations/0001_initial.py +++ b/passbook/factors/otp/migrations/0001_initial.py @@ -9,20 +9,36 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='OTPFactor', + name="OTPFactor", fields=[ - ('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')), - ('enforced', models.BooleanField(default=False, help_text='Enforce enabled OTP for Users this factor applies to.')), + ( + "factor_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Factor", + ), + ), + ( + "enforced", + models.BooleanField( + default=False, + help_text="Enforce enabled OTP for Users this factor applies to.", + ), + ), ], options={ - 'verbose_name': 'OTP Factor', - 'verbose_name_plural': 'OTP Factors', + "verbose_name": "OTP Factor", + "verbose_name_plural": "OTP Factors", }, - bases=('passbook_core.factor',), + bases=("passbook_core.factor",), ), ] diff --git a/passbook/factors/otp/models.py b/passbook/factors/otp/models.py index 31d64df97..daf9fb4a2 100644 --- a/passbook/factors/otp/models.py +++ b/passbook/factors/otp/models.py @@ -9,19 +9,23 @@ from passbook.core.models import Factor, UserSettings class OTPFactor(Factor): """OTP Factor""" - enforced = models.BooleanField(default=False, help_text=('Enforce enabled OTP for Users ' - 'this factor applies to.')) + enforced = models.BooleanField( + default=False, + help_text=("Enforce enabled OTP for Users " "this factor applies to."), + ) - type = 'passbook.factors.otp.factors.OTPFactor' - form = 'passbook.factors.otp.forms.OTPFactorForm' + type = "passbook.factors.otp.factors.OTPFactor" + form = "passbook.factors.otp.forms.OTPFactorForm" def user_settings(self) -> UserSettings: - return UserSettings(_('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings') + return UserSettings( + _("OTP"), "pficon-locked", "passbook_factors_otp:otp-user-settings" + ) def __str__(self): return f"OTP Factor {self.slug}" class Meta: - verbose_name = _('OTP Factor') - verbose_name_plural = _('OTP Factors') + verbose_name = _("OTP Factor") + verbose_name_plural = _("OTP Factors") diff --git a/passbook/factors/otp/settings.py b/passbook/factors/otp/settings.py index 41aa07799..6bd9d8f83 100644 --- a/passbook/factors/otp/settings.py +++ b/passbook/factors/otp/settings.py @@ -1,10 +1,10 @@ """passbook OTP Settings""" MIDDLEWARE = [ - 'django_otp.middleware.OTPMiddleware', + "django_otp.middleware.OTPMiddleware", ] INSTALLED_APPS = [ - 'django_otp', - 'django_otp.plugins.otp_static', - 'django_otp.plugins.otp_totp', + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", ] diff --git a/passbook/factors/otp/urls.py b/passbook/factors/otp/urls.py index 6da05b400..4efc3ca88 100644 --- a/passbook/factors/otp/urls.py +++ b/passbook/factors/otp/urls.py @@ -5,8 +5,8 @@ from django.urls import path from passbook.factors.otp import views urlpatterns = [ - path('', views.UserSettingsView.as_view(), name='otp-user-settings'), - path('qr/', views.QRView.as_view(), name='otp-qr'), - path('enable/', views.EnableView.as_view(), name='otp-enable'), - path('disable/', views.DisableView.as_view(), name='otp-disable'), + path("", views.UserSettingsView.as_view(), name="otp-user-settings"), + path("qr/", views.QRView.as_view(), name="otp-qr"), + path("enable/", views.EnableView.as_view(), name="otp-enable"), + path("disable/", views.DisableView.as_view(), name="otp-disable"), ] diff --git a/passbook/factors/otp/utils.py b/passbook/factors/otp/utils.py index 802c69255..978803747 100644 --- a/passbook/factors/otp/utils.py +++ b/passbook/factors/otp/utils.py @@ -9,9 +9,9 @@ def otpauth_url(accountname, secret, issuer=None, digits=6): # Ensure that the secret parameter is the FIRST parameter of the URI, this # allows Microsoft Authenticator to work. query = [ - ('secret', secret), - ('digits', digits), - ('issuer', 'passbook'), + ("secret", secret), + ("digits", digits), + ("issuer", "passbook"), ] - return 'otpauth://totp/%s:%s?%s' % (issuer, accountname, urlencode(query)) + return "otpauth://totp/%s:%s?%s" % (issuer, accountname, urlencode(query)) diff --git a/passbook/factors/otp/views.py b/passbook/factors/otp/views.py index 3715f5b0c..22a3c23ca 100644 --- a/passbook/factors/otp/views.py +++ b/passbook/factors/otp/views.py @@ -22,25 +22,26 @@ from passbook.factors.otp.utils import otpauth_url from passbook.lib.boilerplate import NeverCacheMixin from passbook.lib.config import CONFIG -OTP_SESSION_KEY = 'passbook_factors_otp_key' -OTP_SETTING_UP_KEY = 'passbook_factors_otp_setup' +OTP_SESSION_KEY = "passbook_factors_otp_key" +OTP_SETTING_UP_KEY = "passbook_factors_otp_setup" LOGGER = get_logger() class UserSettingsView(LoginRequiredMixin, TemplateView): """View for user settings to control OTP""" - template_name = 'otp/user_settings.html' + template_name = "otp/user_settings.html" # TODO: Check if OTP Factor exists and applies to user def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) static = StaticDevice.objects.filter(user=self.request.user, confirmed=True) if static.exists(): - kwargs['static_tokens'] = StaticToken.objects.filter(device=static.first()) \ - .order_by('token') + kwargs["static_tokens"] = StaticToken.objects.filter( + device=static.first() + ).order_by("token") totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True) - kwargs['state'] = totp_devices.exists() and static.exists() + kwargs["state"] = totp_devices.exists() and static.exists() return kwargs @@ -50,44 +51,48 @@ class DisableView(LoginRequiredMixin, View): def get(self, request: HttpRequest) -> HttpResponse: """Delete all the devices for user""" static = get_object_or_404(StaticDevice, user=request.user, confirmed=True) - static_tokens = StaticToken.objects.filter(device=static).order_by('token') + static_tokens = StaticToken.objects.filter(device=static).order_by("token") totp = TOTPDevice.objects.filter(user=request.user, confirmed=True) static.delete() totp.delete() for token in static_tokens: token.delete() - messages.success(request, 'Successfully disabled OTP') + messages.success(request, "Successfully disabled OTP") # Create event with email notification - Event.new(EventAction.CUSTOM, message='User disabled OTP.').from_http(request) - return redirect(reverse('passbook_factors_otp:otp-user-settings')) + Event.new(EventAction.CUSTOM, message="User disabled OTP.").from_http(request) + return redirect(reverse("passbook_factors_otp:otp-user-settings")) class EnableView(LoginRequiredMixin, FormView): """View to set up OTP""" - title = _('Set up OTP') + title = _("Set up OTP") form_class = OTPSetupForm - template_name = 'login/form.html' + template_name = "login/form.html" totp_device = None static_device = None # TODO: Check if OTP Factor exists and applies to user def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.y('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Configure OTP') - kwargs['primary_action'] = _('Setup') + kwargs["config"] = CONFIG.y("passbook") + kwargs["is_login"] = True + kwargs["title"] = _("Configure OTP") + kwargs["primary_action"] = _("Setup") return super().get_context_data(**kwargs) def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # Check if user has TOTP setup already - finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True) - finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True) + finished_totp_devices = TOTPDevice.objects.filter( + user=request.user, confirmed=True + ) + finished_static_devices = StaticDevice.objects.filter( + user=request.user, confirmed=True + ) if finished_totp_devices.exists() and finished_static_devices.exists(): - messages.error(request, _('You already have TOTP enabled!')) + messages.error(request, _("You already have TOTP enabled!")) del request.session[OTP_SETTING_UP_KEY] - return redirect('passbook_factors_otp:otp-user-settings') + return redirect("passbook_factors_otp:otp-user-settings") request.session[OTP_SETTING_UP_KEY] = True # Check if there's an unconfirmed device left to set up totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False) @@ -107,22 +112,24 @@ class EnableView(LoginRequiredMixin, FormView): # Create 9 tokens and save them # TODO: Send static tokens via E-Mail for _counter in range(0, 9): - token = StaticToken(device=self.static_device, token=StaticToken.random_token()) + token = StaticToken( + device=self.static_device, token=StaticToken.random_token() + ) token.save() else: self.static_device = static_devices.first() # Somehow convert the generated key to base32 for the QR code - rawkey = unhexlify(self.totp_device.key.encode('ascii')) + rawkey = unhexlify(self.totp_device.key.encode("ascii")) request.session[OTP_SESSION_KEY] = b32encode(rawkey).decode("utf-8") return super().dispatch(request, *args, **kwargs) def get_form(self, form_class=None): form = super().get_form(form_class=form_class) form.device = self.totp_device - form.fields['qr_code'].initial = reverse('passbook_factors_otp:otp-qr') + form.fields["qr_code"].initial = reverse("passbook_factors_otp:otp-qr") tokens = [(x.token, x.token) for x in self.static_device.token_set.all()] - form.fields['tokens'].choices = tokens + form.fields["tokens"].choices = tokens return form def form_valid(self, form): @@ -133,8 +140,10 @@ class EnableView(LoginRequiredMixin, FormView): self.static_device.confirmed = True self.static_device.save() del self.request.session[OTP_SETTING_UP_KEY] - Event.new(EventAction.CUSTOM, message='User enabled OTP.').from_http(self.request) - return redirect('passbook_factors_otp:otp-user-settings') + Event.new(EventAction.CUSTOM, message="User enabled OTP.").from_http( + self.request + ) + return redirect("passbook_factors_otp:otp-user-settings") class QRView(NeverCacheMixin, View): @@ -151,6 +160,6 @@ class QRView(NeverCacheMixin, View): url = otpauth_url(accountname=request.user.username, secret=key) # Make and return QR code img = make(url, image_factory=SvgPathImage) - resp = HttpResponse(content_type='image/svg+xml; charset=utf-8') + resp = HttpResponse(content_type="image/svg+xml; charset=utf-8") img.save(resp) return resp diff --git a/passbook/factors/password/admin.py b/passbook/factors/password/admin.py index 1ff90534f..2da557451 100644 --- a/passbook/factors/password/admin.py +++ b/passbook/factors/password/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_factors_password') +admin_autoregister("passbook_factors_password") diff --git a/passbook/factors/password/api.py b/passbook/factors/password/api.py index 4306f46a1..1c8b3e407 100644 --- a/passbook/factors/password/api.py +++ b/passbook/factors/password/api.py @@ -11,8 +11,16 @@ class PasswordFactorSerializer(ModelSerializer): class Meta: model = PasswordFactor - fields = ['pk', 'name', 'slug', 'order', 'enabled', - 'backends', 'password_policies', 'reset_factors'] + fields = [ + "pk", + "name", + "slug", + "order", + "enabled", + "backends", + "password_policies", + "reset_factors", + ] class PasswordFactorViewSet(ModelViewSet): diff --git a/passbook/factors/password/apps.py b/passbook/factors/password/apps.py index 2ab933c0d..207d91f9a 100644 --- a/passbook/factors/password/apps.py +++ b/passbook/factors/password/apps.py @@ -7,9 +7,9 @@ from django.apps import AppConfig class PassbookFactorPasswordConfig(AppConfig): """passbook password factor config""" - name = 'passbook.factors.password' - label = 'passbook_factors_password' - verbose_name = 'passbook Factors.Password' + name = "passbook.factors.password" + label = "passbook_factors_password" + verbose_name = "passbook Factors.Password" def ready(self): - import_module('passbook.factors.password.signals') + import_module("passbook.factors.password.signals") diff --git a/passbook/factors/password/exceptions.py b/passbook/factors/password/exceptions.py index b04d2668f..f561ec5ac 100644 --- a/passbook/factors/password/exceptions.py +++ b/passbook/factors/password/exceptions.py @@ -1,5 +1,6 @@ """passbook password policy exceptions""" + class PasswordPolicyInvalid(Exception): """Exception raised when a Password Policy fails""" diff --git a/passbook/factors/password/factor.py b/passbook/factors/password/factor.py index ed2a2244a..0aaf7c7d0 100644 --- a/passbook/factors/password/factor.py +++ b/passbook/factors/password/factor.py @@ -33,11 +33,11 @@ def authenticate(request, backends, **credentials) -> Optional[User]: LOGGER.warning("Backend doesn't accept our arguments", backend=backend) # This backend doesn't accept these credentials as arguments. Try the next one. continue - LOGGER.debug('Attempting authentication...', backend=backend) + LOGGER.debug("Attempting authentication...", backend=backend) try: user = backend.authenticate(request, **credentials) except PermissionDenied: - LOGGER.debug('Backend threw PermissionDenied', backend=backend) + LOGGER.debug("Backend threw PermissionDenied", backend=backend) # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: @@ -47,29 +47,35 @@ def authenticate(request, backends, **credentials) -> Optional[User]: return user # The credentials supplied are invalid to all backends, fire signal - user_login_failed.send(sender=__name__, credentials=_clean_credentials( - credentials), request=request) + user_login_failed.send( + sender=__name__, credentials=_clean_credentials(credentials), request=request + ) + class PasswordFactor(FormView, AuthenticationFactor): """Authentication factor which authenticates against django's AuthBackend""" form_class = PasswordForm - template_name = 'login/factors/backend.html' + template_name = "login/factors/backend.html" def form_valid(self, form): """Authenticate against django's authentication backend""" - uid_fields = CONFIG.y('passbook.uid_fields') + uid_fields = CONFIG.y("passbook.uid_fields") kwargs = { - 'password': form.cleaned_data.get('password'), + "password": form.cleaned_data.get("password"), } for uid_field in uid_fields: kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) try: - user = authenticate(self.request, self.authenticator.current_factor.backends, **kwargs) + user = authenticate( + self.request, self.authenticator.current_factor.backends, **kwargs + ) if user: # User instance returned from authenticate() has .backend property set self.authenticator.pending_user = user - self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend + self.request.session[ + AuthenticationView.SESSION_USER_BACKEND + ] = user.backend return self.authenticator.user_ok() # No user was found -> invalid credentials LOGGER.debug("Invalid credentials") diff --git a/passbook/factors/password/forms.py b/passbook/factors/password/forms.py index ae0804445..d9def8778 100644 --- a/passbook/factors/password/forms.py +++ b/passbook/factors/password/forms.py @@ -13,17 +13,23 @@ def get_authentication_backends(): """Return all available authentication backends as tuple set""" for backend in settings.AUTHENTICATION_BACKENDS: klass = path_to_class(backend) - yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__)) + yield backend, getattr( + klass(), "name", "%s (%s)" % (klass.__name__, klass.__module__) + ) class PasswordForm(forms.Form): """Password authentication form""" - password = forms.CharField(widget=forms.PasswordInput(attrs={ - 'placeholder': _('Password'), - 'autofocus': 'autofocus', - 'autocomplete': 'current-password' - })) + password = forms.CharField( + widget=forms.PasswordInput( + attrs={ + "placeholder": _("Password"), + "autofocus": "autofocus", + "autocomplete": "current-password", + } + ) + ) class PasswordFactorForm(forms.ModelForm): @@ -32,13 +38,14 @@ class PasswordFactorForm(forms.ModelForm): class Meta: model = PasswordFactor - fields = GENERAL_FIELDS + ['backends', 'password_policies', 'reset_factors'] + fields = GENERAL_FIELDS + ["backends", "password_policies", "reset_factors"] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False), - 'backends': FilteredSelectMultiple(_('backends'), False, - choices=get_authentication_backends()), - 'password_policies': FilteredSelectMultiple(_('password policies'), False), - 'reset_factors': FilteredSelectMultiple(_('reset factors'), False), + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), + "backends": FilteredSelectMultiple( + _("backends"), False, choices=get_authentication_backends() + ), + "password_policies": FilteredSelectMultiple(_("password policies"), False), + "reset_factors": FilteredSelectMultiple(_("reset factors"), False), } diff --git a/passbook/factors/password/migrations/0001_initial.py b/passbook/factors/password/migrations/0001_initial.py index 68cfaa8a0..58be06b49 100644 --- a/passbook/factors/password/migrations/0001_initial.py +++ b/passbook/factors/password/migrations/0001_initial.py @@ -10,21 +10,39 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PasswordFactor', + name="PasswordFactor", fields=[ - ('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')), - ('backends', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)), - ('password_policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')), + ( + "factor_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Factor", + ), + ), + ( + "backends", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), + ( + "password_policies", + models.ManyToManyField(blank=True, to="passbook_core.Policy"), + ), ], options={ - 'verbose_name': 'Password Factor', - 'verbose_name_plural': 'Password Factors', + "verbose_name": "Password Factor", + "verbose_name_plural": "Password Factors", }, - bases=('passbook_core.factor',), + bases=("passbook_core.factor",), ), ] diff --git a/passbook/factors/password/migrations/0002_auto_20191007_1411.py b/passbook/factors/password/migrations/0002_auto_20191007_1411.py index b7e15c08c..3cafedf26 100644 --- a/passbook/factors/password/migrations/0002_auto_20191007_1411.py +++ b/passbook/factors/password/migrations/0002_auto_20191007_1411.py @@ -8,18 +8,17 @@ def create_initial_factor(apps, schema_editor): PasswordFactor = apps.get_model("passbook_factors_password", "PasswordFactor") if not PasswordFactor.objects.exists(): PasswordFactor.objects.create( - name='password', - slug='password', + name="password", + slug="password", order=0, - backends=['django.contrib.auth.backends.ModelBackend'] + backends=["django.contrib.auth.backends.ModelBackend"], ) + class Migration(migrations.Migration): dependencies = [ - ('passbook_factors_password', '0001_initial'), + ("passbook_factors_password", "0001_initial"), ] - operations = [ - migrations.RunPython(create_initial_factor) - ] + operations = [migrations.RunPython(create_initial_factor)] diff --git a/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py b/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py index 19531171d..b64e40df8 100644 --- a/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py +++ b/passbook/factors/password/migrations/0003_passwordfactor_reset_factors.py @@ -6,14 +6,16 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0001_initial'), - ('passbook_factors_password', '0002_auto_20191007_1411'), + ("passbook_core", "0001_initial"), + ("passbook_factors_password", "0002_auto_20191007_1411"), ] operations = [ migrations.AddField( - model_name='passwordfactor', - name='reset_factors', - field=models.ManyToManyField(blank=True, related_name='reset_factors', to='passbook_core.Factor'), + model_name="passwordfactor", + name="reset_factors", + field=models.ManyToManyField( + blank=True, related_name="reset_factors", to="passbook_core.Factor" + ), ), ] diff --git a/passbook/factors/password/models.py b/passbook/factors/password/models.py index 513139c49..b5ed6bdf5 100644 --- a/passbook/factors/password/models.py +++ b/passbook/factors/password/models.py @@ -11,14 +11,17 @@ class PasswordFactor(Factor): backends = ArrayField(models.TextField()) password_policies = models.ManyToManyField(Policy, blank=True) - reset_factors = models.ManyToManyField(Factor, blank=True, related_name='reset_factors') + reset_factors = models.ManyToManyField( + Factor, blank=True, related_name="reset_factors" + ) - type = 'passbook.factors.password.factor.PasswordFactor' - form = 'passbook.factors.password.forms.PasswordFactorForm' + type = "passbook.factors.password.factor.PasswordFactor" + form = "passbook.factors.password.forms.PasswordFactorForm" def user_settings(self): - return UserSettings(_('Change Password'), 'pficon-key', - 'passbook_core:user-change-password') + return UserSettings( + _("Change Password"), "pficon-key", "passbook_core:user-change-password" + ) def password_passes(self, user: User) -> bool: """Return true if user's password passes, otherwise False or raise Exception""" @@ -32,5 +35,5 @@ class PasswordFactor(Factor): class Meta: - verbose_name = _('Password Factor') - verbose_name_plural = _('Password Factors') + verbose_name = _("Password Factor") + verbose_name_plural = _("Password Factors") diff --git a/passbook/factors/password/signals.py b/passbook/factors/password/signals.py index 3a175da33..ffdacda86 100644 --- a/passbook/factors/password/signals.py +++ b/passbook/factors/password/signals.py @@ -10,10 +10,13 @@ def password_policy_checker(sender, password, **_): """Run password through all password policies which are applied to the user""" from passbook.factors.password.models import PasswordFactor from passbook.policies.engine import PolicyEngine - setattr(sender, '__password__', password) - _all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order') + + setattr(sender, "__password__", password) + _all_factors = PasswordFactor.objects.filter(enabled=True).order_by("order") for factor in _all_factors: - policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses(), sender) + policy_engine = PolicyEngine( + factor.password_policies.all().select_subclasses(), sender + ) policy_engine.build() passing, messages = policy_engine.result if not passing: diff --git a/passbook/factors/tests.py b/passbook/factors/tests.py index df447d930..3622321e9 100644 --- a/passbook/factors/tests.py +++ b/passbook/factors/tests.py @@ -18,28 +18,33 @@ class TestFactorAuthentication(TestCase): def setUp(self): super().setUp() - self.password = ''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(8)) - self.factor, _ = PasswordFactor.objects.get_or_create(slug='password', defaults={ - 'name': 'password', - 'slug': 'password', - 'order': 0, - 'backends': ['django.contrib.auth.backends.ModelBackend'] - }) - self.user = User.objects.create_user(username='test', - email='test@test.test', - password=self.password) + self.password = "".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ) + self.factor, _ = PasswordFactor.objects.get_or_create( + slug="password", + defaults={ + "name": "password", + "slug": "password", + "order": 0, + "backends": ["django.contrib.auth.backends.ModelBackend"], + }, + ) + self.user = User.objects.create_user( + username="test", email="test@test.test", password=self.password + ) def test_unauthenticated_raw(self): """test direct call to AuthenticationView""" - response = self.client.get(reverse('passbook_core:auth-process')) + response = self.client.get(reverse("passbook_core:auth-process")) # Response should be 302 since no pending user is set self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse('passbook_core:auth-login')) + self.assertEqual(response.url, reverse("passbook_core:auth-login")) def test_unauthenticated_prepared(self): """test direct call but with pending_uesr in session""" - request = RequestFactory().get(reverse('passbook_core:auth-process')) + request = RequestFactory().get(reverse("passbook_core:auth-process")) request.user = AnonymousUser() request.session = {} request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk @@ -51,31 +56,31 @@ class TestFactorAuthentication(TestCase): """Test with all factors disabled""" self.factor.enabled = False self.factor.save() - request = RequestFactory().get(reverse('passbook_core:auth-process')) + request = RequestFactory().get(reverse("passbook_core:auth-process")) request.user = AnonymousUser() request.session = {} request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk response = AuthenticationView.as_view()(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse('passbook_core:auth-denied')) + self.assertEqual(response.url, reverse("passbook_core:auth-denied")) self.factor.enabled = True self.factor.save() def test_authenticated(self): """Test with already logged in user""" self.client.force_login(self.user) - response = self.client.get(reverse('passbook_core:auth-process')) + response = self.client.get(reverse("passbook_core:auth-process")) # Response should be 302 since no pending user is set self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse('passbook_core:overview')) + self.assertEqual(response.url, reverse("passbook_core:overview")) self.client.logout() def test_unauthenticated_post(self): """Test post request as unauthenticated user""" - request = RequestFactory().post(reverse('passbook_core:auth-process'), data={ - 'password': self.password - }) + request = RequestFactory().post( + reverse("passbook_core:auth-process"), data={"password": self.password} + ) request.user = AnonymousUser() middleware = SessionMiddleware() middleware.process_request(request) @@ -84,14 +89,15 @@ class TestFactorAuthentication(TestCase): response = AuthenticationView.as_view()(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse('passbook_core:overview')) + self.assertEqual(response.url, reverse("passbook_core:overview")) self.client.logout() def test_unauthenticated_post_invalid(self): """Test post request as unauthenticated user""" - request = RequestFactory().post(reverse('passbook_core:auth-process'), data={ - 'password': self.password + 'a' - }) + request = RequestFactory().post( + reverse("passbook_core:auth-process"), + data={"password": self.password + "a"}, + ) request.user = AnonymousUser() middleware = SessionMiddleware() middleware.process_request(request) @@ -104,12 +110,10 @@ class TestFactorAuthentication(TestCase): def test_multifactor(self): """Test view with multiple active factors""" - DummyFactor.objects.get_or_create(name='dummy', - slug='dummy', - order=1) - request = RequestFactory().post(reverse('passbook_core:auth-process'), data={ - 'password': self.password - }) + DummyFactor.objects.get_or_create(name="dummy", slug="dummy", order=1) + request = RequestFactory().post( + reverse("passbook_core:auth-process"), data={"password": self.password} + ) request.user = AnonymousUser() middleware = SessionMiddleware() middleware.process_request(request) @@ -120,16 +124,16 @@ class TestFactorAuthentication(TestCase): session_copy = request.session.items() self.assertEqual(response.status_code, 302) # Verify view redirects to itself after auth - self.assertEqual(response.url, reverse('passbook_core:auth-process')) + self.assertEqual(response.url, reverse("passbook_core:auth-process")) # Run another request with same session which should result in a logged in user - request = RequestFactory().post(reverse('passbook_core:auth-process')) + request = RequestFactory().post(reverse("passbook_core:auth-process")) request.user = AnonymousUser() middleware = SessionMiddleware() middleware.process_request(request) for key, value in session_copy: request.session[key] = value - request.session.save() # pylint: disable=no-member + request.session.save() # pylint: disable=no-member response = AuthenticationView.as_view()(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse('passbook_core:overview')) + self.assertEqual(response.url, reverse("passbook_core:overview")) diff --git a/passbook/factors/view.py b/passbook/factors/view.py index 1b5e7a498..9a6f05dc4 100644 --- a/passbook/factors/view.py +++ b/passbook/factors/view.py @@ -16,25 +16,25 @@ from passbook.policies.engine import PolicyEngine LOGGER = get_logger() # Argument used to redirect user after login -NEXT_ARG_NAME = 'next' +NEXT_ARG_NAME = "next" def _redirect_with_qs(view, get_query_set=None): """Wrapper to redirect whilst keeping GET Parameters""" target = reverse(view) if get_query_set: - target += '?' + urlencode(get_query_set) + target += "?" + urlencode(get_query_set) return redirect(target) class AuthenticationView(UserPassesTestMixin, View): """Wizard-like Multi-factor authenticator""" - SESSION_FACTOR = 'passbook_factor' - SESSION_PENDING_FACTORS = 'passbook_pending_factors' - SESSION_PENDING_USER = 'passbook_pending_user' - SESSION_USER_BACKEND = 'passbook_user_backend' - SESSION_IS_SSO_LOGIN = 'passbook_sso_login' + SESSION_FACTOR = "passbook_factor" + SESSION_PENDING_FACTORS = "passbook_pending_factors" + SESSION_PENDING_USER = "passbook_pending_user" + SESSION_USER_BACKEND = "passbook_user_backend" + SESSION_IS_SSO_LOGIN = "passbook_sso_login" pending_user: User pending_factors: List[Tuple[str, str]] = [] @@ -52,8 +52,8 @@ class AuthenticationView(UserPassesTestMixin, View): if NEXT_ARG_NAME in self.request.GET: return redirect(self.request.GET.get(NEXT_ARG_NAME)) if self.request.user.is_authenticated: - return _redirect_with_qs('passbook_core:overview', self.request.GET) - return _redirect_with_qs('passbook_core:auth-login', self.request.GET) + return _redirect_with_qs("passbook_core:overview", self.request.GET) + return _redirect_with_qs("passbook_core:auth-login", self.request.GET) def get_pending_factors(self): """Loading pending factors from Database or load from session variable""" @@ -62,12 +62,19 @@ class AuthenticationView(UserPassesTestMixin, View): return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] # Get an initial list of factors which are currently enabled # and apply to the current user. We check policies here and block the request - _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() + _all_factors = ( + Factor.objects.filter(enabled=True).order_by("order").select_subclasses() + ) pending_factors = [] for factor in _all_factors: - LOGGER.debug("Checking if factor applies to user", - factor=factor, user=self.pending_user) - policy_engine = PolicyEngine(factor.policies.all(), self.pending_user, self.request) + LOGGER.debug( + "Checking if factor applies to user", + factor=factor, + user=self.pending_user, + ) + policy_engine = PolicyEngine( + factor.policies.all(), self.pending_user, self.request + ) policy_engine.build() if policy_engine.passing: pending_factors.append((factor.uuid.hex, factor.type)) @@ -81,7 +88,8 @@ class AuthenticationView(UserPassesTestMixin, View): return self.handle_no_permission() # Extract pending user from session (only remember uid) self.pending_user = get_object_or_404( - User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER]) + User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER] + ) self.pending_factors = self.get_pending_factors() # Read and instantiate factor from session factor_uuid, factor_class = None, None @@ -95,9 +103,13 @@ class AuthenticationView(UserPassesTestMixin, View): return self.user_invalid() factor_uuid, factor_class = self.pending_factors[0] else: - factor_uuid, factor_class = request.session[AuthenticationView.SESSION_FACTOR] + factor_uuid, factor_class = request.session[ + AuthenticationView.SESSION_FACTOR + ] # Lookup current factor object - self.current_factor = Factor.objects.filter(uuid=factor_uuid).select_subclasses().first() + self.current_factor = ( + Factor.objects.filter(uuid=factor_uuid).select_subclasses().first() + ) # Instantiate Next Factor and pass request factor = path_to_class(factor_class) self._current_factor_class = factor(self) @@ -107,32 +119,43 @@ class AuthenticationView(UserPassesTestMixin, View): def get(self, request, *args, **kwargs): """pass get request to current factor""" - LOGGER.debug("Passing GET", view_class=class_to_path(self._current_factor_class.__class__)) + LOGGER.debug( + "Passing GET", + view_class=class_to_path(self._current_factor_class.__class__), + ) return self._current_factor_class.get(request, *args, **kwargs) def post(self, request, *args, **kwargs): """pass post request to current factor""" - LOGGER.debug("Passing POST", view_class=class_to_path(self._current_factor_class.__class__)) + LOGGER.debug( + "Passing POST", + view_class=class_to_path(self._current_factor_class.__class__), + ) return self._current_factor_class.post(request, *args, **kwargs) def user_ok(self): """Redirect to next Factor""" - LOGGER.debug("Factor passed", - factor_class=class_to_path(self._current_factor_class.__class__)) + LOGGER.debug( + "Factor passed", + factor_class=class_to_path(self._current_factor_class.__class__), + ) # Remove passed factor from pending factors - current_factor_tuple = (self.current_factor.uuid.hex, - class_to_path(self._current_factor_class.__class__)) + current_factor_tuple = ( + self.current_factor.uuid.hex, + class_to_path(self._current_factor_class.__class__), + ) if current_factor_tuple in self.pending_factors: self.pending_factors.remove(current_factor_tuple) next_factor = None if self.pending_factors: next_factor = self.pending_factors.pop() # Save updated pening_factor list to session - self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \ - self.pending_factors + self.request.session[ + AuthenticationView.SESSION_PENDING_FACTORS + ] = self.pending_factors self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor LOGGER.debug("Rendering Factor", next_factor=next_factor) - return _redirect_with_qs('passbook_core:auth-process', self.request.GET) + return _redirect_with_qs("passbook_core:auth-process", self.request.GET) # User passed all factors LOGGER.debug("User passed all factors, logging in", user=self.pending_user) return self._user_passed() @@ -142,7 +165,7 @@ class AuthenticationView(UserPassesTestMixin, View): This should only be shown if user authenticated successfully, but is disabled/locked/etc""" LOGGER.debug("User invalid") self.cleanup() - return _redirect_with_qs('passbook_core:auth-denied', self.request.GET) + return _redirect_with_qs("passbook_core:auth-denied", self.request.GET) def _user_passed(self): """User Successfully passed all factors""" @@ -154,12 +177,16 @@ class AuthenticationView(UserPassesTestMixin, View): next_param = self.request.GET.get(NEXT_ARG_NAME, None) if next_param and not is_url_absolute(next_param): return redirect(next_param) - return _redirect_with_qs('passbook_core:overview') + return _redirect_with_qs("passbook_core:overview") def cleanup(self): """Remove temporary data from session""" - session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS, - self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ] + session_keys = [ + self.SESSION_FACTOR, + self.SESSION_PENDING_FACTORS, + self.SESSION_PENDING_USER, + self.SESSION_USER_BACKEND, + ] for key in session_keys: if key in self.request.session: del self.request.session[key] diff --git a/passbook/lib/apps.py b/passbook/lib/apps.py index e01135924..301735ee6 100644 --- a/passbook/lib/apps.py +++ b/passbook/lib/apps.py @@ -5,6 +5,6 @@ from django.apps import AppConfig class PassbookLibConfig(AppConfig): """passbook lib app config""" - name = 'passbook.lib' - label = 'passbook_lib' - verbose_name = 'passbook lib' + name = "passbook.lib" + label = "passbook_lib" + verbose_name = "passbook lib" diff --git a/passbook/lib/boilerplate.py b/passbook/lib/boilerplate.py index f21cad45e..38c12800f 100644 --- a/passbook/lib/boilerplate.py +++ b/passbook/lib/boilerplate.py @@ -3,7 +3,7 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache -class NeverCacheMixin(): +class NeverCacheMixin: """Use never_cache as mixin for CBV""" @method_decorator(never_cache) diff --git a/passbook/lib/config.py b/passbook/lib/config.py index 07abcea70..fe9871d1b 100644 --- a/passbook/lib/config.py +++ b/passbook/lib/config.py @@ -11,14 +11,12 @@ from django.conf import ImproperlyConfigured from django.utils.autoreload import autoreload_started from structlog import get_logger -SEARCH_PATHS = [ - 'passbook/lib/default.yml', - '/etc/passbook/config.yml', - '', -] + glob('/etc/passbook/config.d/*.yml', recursive=True) +SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", "",] + glob( + "/etc/passbook/config.d/*.yml", recursive=True +) LOGGER = get_logger() -ENV_PREFIX = 'PASSBOOK' -ENVIRONMENT = os.getenv(f'{ENV_PREFIX}_ENV', 'local') +ENV_PREFIX = "PASSBOOK" +ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") class ConfigLoader: @@ -34,7 +32,7 @@ class ConfigLoader: def __init__(self): super().__init__() - base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..')) + base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../..")) for path in SEARCH_PATHS: # Check if path is relative, and if so join with base_dir if not os.path.isabs(path): @@ -44,8 +42,10 @@ class ConfigLoader: self.update_from_file(path) elif os.path.isdir(path) and os.path.exists(path): # Path is an existing dir, so we try to read the env config from it - env_paths = [os.path.join(path, ENVIRONMENT + '.yml'), - os.path.join(path, ENVIRONMENT + '.env.yml')] + env_paths = [ + os.path.join(path, ENVIRONMENT + ".yml"), + os.path.join(path, ENVIRONMENT + ".env.yml"), + ] for env_file in env_paths: if os.path.isfile(env_file) and os.path.exists(env_file): # Update config with env file @@ -66,7 +66,7 @@ class ConfigLoader: def parse_uri(self, value): """Parse string values which start with a URI""" url = urlparse(value) - if url.scheme == 'env': + if url.scheme == "env": value = os.getenv(url.netloc, url.query) return value @@ -81,7 +81,7 @@ class ConfigLoader: except yaml.YAMLError as exc: raise ImproperlyConfigured from exc except PermissionError as exc: - LOGGER.warning('Permission denied while reading file', path=path, error=exc) + LOGGER.warning("Permission denied while reading file", path=path, error=exc) def update_from_dict(self, update: dict): """Update config from dict""" @@ -94,10 +94,10 @@ class ConfigLoader: for key, value in os.environ.items(): if not key.startswith(ENV_PREFIX): continue - relative_key = key.replace(f"{ENV_PREFIX}_", '').replace('__', '.').lower() + relative_key = key.replace(f"{ENV_PREFIX}_", "").replace("__", ".").lower() # Recursively convert path from a.b.c into outer[a][b][c] current_obj = outer - dot_parts = relative_key.split('.') + dot_parts = relative_key.split(".") for dot_part in dot_parts[:-1]: if dot_part not in current_obj: current_obj[dot_part] = {} @@ -122,7 +122,7 @@ class ConfigLoader: return self.__config # pylint: disable=invalid-name - def y(self, path: str, default=None, sep='.') -> Any: + def y(self, path: str, default=None, sep=".") -> Any: """Access attribute by using yaml path""" # Walk sub_dicts before parsing path root = self.raw @@ -138,7 +138,7 @@ class ConfigLoader: def y_bool(self, path: str, default=False) -> bool: """Wrapper for y that converts value into boolean""" - return str(self.y(path, default)).lower() == 'true' + return str(self.y(path, default)).lower() == "true" CONFIG = ConfigLoader() diff --git a/passbook/lib/decorators.py b/passbook/lib/decorators.py index 62e20ca45..ab4b23f8c 100644 --- a/passbook/lib/decorators.py +++ b/passbook/lib/decorators.py @@ -7,8 +7,8 @@ from django.urls import reverse from django.utils.functional import wraps from django.utils.http import urlencode -RE_AUTH_KEY = getattr(settings, 'RE_AUTH_KEY', 'passbook_require_re_auth_done') -RE_AUTH_MARGAIN = getattr(settings, 'RE_AUTH_MARGAIN', 300) +RE_AUTH_KEY = getattr(settings, "RE_AUTH_KEY", "passbook_require_re_auth_done") +RE_AUTH_MARGAIN = getattr(settings, "RE_AUTH_MARGAIN", 300) def reauth_required(view_function): @@ -21,25 +21,31 @@ def reauth_required(view_function): request = args[0] if args else None # Check if user is authenticated at all if not request or not request.user or not request.user.is_authenticated: - return redirect(reverse('account-login')) + return redirect(reverse("account-login")) now = timestamp() - if RE_AUTH_KEY in request.session and \ - request.session[RE_AUTH_KEY] < (now - RE_AUTH_MARGAIN): + if RE_AUTH_KEY in request.session and request.session[RE_AUTH_KEY] < ( + now - RE_AUTH_MARGAIN + ): # Timestamp in session but expired del request.session[RE_AUTH_KEY] if RE_AUTH_KEY not in request.session: # Timestamp not in session, force user to reauth - return redirect(reverse('account-reauth') + '?' + urlencode({'next': request.path})) + return redirect( + reverse("account-reauth") + "?" + urlencode({"next": request.path}) + ) - if RE_AUTH_KEY in request.session and \ - request.session[RE_AUTH_KEY] >= (now - RE_AUTH_MARGAIN) and \ - request.session[RE_AUTH_KEY] <= now: + if ( + RE_AUTH_KEY in request.session + and request.session[RE_AUTH_KEY] >= (now - RE_AUTH_MARGAIN) + and request.session[RE_AUTH_KEY] <= now + ): # Timestamp in session and valid return view_function(*args, **kwargs) # This should never be reached, just return False return False # pragma: no cover + return wrap diff --git a/passbook/lib/fields.py b/passbook/lib/fields.py index d2ef82f87..09dbcb3ec 100644 --- a/passbook/lib/fields.py +++ b/passbook/lib/fields.py @@ -10,7 +10,9 @@ from passbook.lib.widgets import DynamicArrayWidget class DynamicArrayField(forms.Field): """Show array field as a dynamic amount of textboxes""" - default_error_messages = {"item_invalid": "Item %(nth)s in the array did not validate: "} + default_error_messages = { + "item_invalid": "Item %(nth)s in the array did not validate: " + } def __init__(self, base_field, **kwargs): self.base_field = base_field @@ -28,8 +30,10 @@ class DynamicArrayField(forms.Field): except forms.ValidationError as error: errors.append( prefix_validation_error( - error, self.error_messages["item_invalid"], - code="item_invalid", params={"nth": index} + error, + self.error_messages["item_invalid"], + code="item_invalid", + params={"nth": index}, ) ) if errors: diff --git a/passbook/lib/models.py b/passbook/lib/models.py index 81939b7cc..231226e1c 100644 --- a/passbook/lib/models.py +++ b/passbook/lib/models.py @@ -6,6 +6,7 @@ from django.db import models class CreatedUpdatedModel(models.Model): """Base Abstract Model to save created and update""" + created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) diff --git a/passbook/lib/sentry.py b/passbook/lib/sentry.py index 98b833292..ec5d475a1 100644 --- a/passbook/lib/sentry.py +++ b/passbook/lib/sentry.py @@ -12,6 +12,7 @@ def before_send(event, hint): from billiard.exceptions import WorkerLostError from django.core.exceptions import DisallowedHost from botocore.client import ClientError + ignored_classes = ( OperationalError, ConnectionInterrupted, @@ -22,10 +23,10 @@ def before_send(event, hint): DisallowedHost, ConnectionResetError, KeyboardInterrupt, - ClientError + ClientError, ) - if 'exc_info' in hint: - _exc_type, exc_value, _ = hint['exc_info'] + if "exc_info" in hint: + _exc_type, exc_value, _ = hint["exc_info"] if isinstance(exc_value, ignored_classes): LOGGER.info("Supressing error %r", exc_value) return None diff --git a/passbook/lib/tasks.py b/passbook/lib/tasks.py index cc36f0ef5..d240d8337 100644 --- a/passbook/lib/tasks.py +++ b/passbook/lib/tasks.py @@ -6,8 +6,9 @@ from passbook.root.celery import CELERY_APP LOGGER = get_logger() + @CELERY_APP.task() def backup_database(): """Backup database""" - management.call_command('dbbackup') - LOGGER.info('Successfully backed up database.') + management.call_command("dbbackup") + LOGGER.info("Successfully backed up database.") diff --git a/passbook/lib/templatetags/inline.py b/passbook/lib/templatetags/inline.py index 4a0c8bae9..9e805e523 100644 --- a/passbook/lib/templatetags/inline.py +++ b/passbook/lib/templatetags/inline.py @@ -10,11 +10,11 @@ register = template.Library() @register.simple_tag() def inline_static(path): """Inline static asset. If file is binary, return b64 representation""" - prefix = 'data:image/svg+xml;utf8,' - data = '' - full_path = settings.STATIC_ROOT + '/' + path + prefix = "data:image/svg+xml;utf8," + data = "" + full_path = settings.STATIC_ROOT + "/" + path if os.path.exists(full_path): - if full_path.endswith('.svg'): + if full_path.endswith(".svg"): with open(full_path) as _file: data = _file.read() return prefix + data diff --git a/passbook/lib/templatetags/is_active.py b/passbook/lib/templatetags/is_active.py index 9a5ac4cd8..a40ee4c3d 100644 --- a/passbook/lib/templatetags/is_active.py +++ b/passbook/lib/templatetags/is_active.py @@ -10,22 +10,23 @@ LOGGER = get_logger() @register.simple_tag(takes_context=True) def is_active(context, *args, **kwargs): """Return whether a navbar link is active or not.""" - request = context.get('request') - app_name = kwargs.get('app_name', None) + request = context.get("request") + app_name = kwargs.get("app_name", None) if not request.resolver_match: - return '' + return "" for url in args: - short_url = url.split(':')[1] if ':' in url else url + short_url = url.split(":")[1] if ":" in url else url # Check if resolver_match matches - if request.resolver_match.url_name.startswith(url) or \ - request.resolver_match.url_name.startswith(short_url): + if request.resolver_match.url_name.startswith( + url + ) or request.resolver_match.url_name.startswith(short_url): # Monkeypatch app_name: urls from core have app_name == '' # since the root urlpatterns have no namespace if app_name and request.resolver_match.app_name == app_name: - return 'active' + return "active" if app_name is None: - return 'active' - return '' + return "active" + return "" @register.simple_tag(takes_context=True) @@ -33,26 +34,28 @@ def is_active_url(context, view): """Return whether a navbar link is active or not.""" # matching_url = reverse(view, args=args, kwargs=kwargs) - request = context.get('request') - current_full_url = f"{request.resolver_match.app_name}:{request.resolver_match.url_name}" + request = context.get("request") + current_full_url = ( + f"{request.resolver_match.app_name}:{request.resolver_match.url_name}" + ) if not request.resolver_match: - return '' + return "" if current_full_url == view: - return 'active' + return "active" # if matching_url == request.path: # return 'active' - return '' + return "" @register.simple_tag(takes_context=True) def is_active_app(context, *args): """Return True if current link is from app""" - request = context.get('request') + request = context.get("request") if not request.resolver_match: - return '' + return "" for app_name in args: if request.resolver_match.app_name == app_name: - return 'active' - return '' + return "active" + return "" diff --git a/passbook/lib/templatetags/utils.py b/passbook/lib/templatetags/utils.py index 72e4aea9a..5f2198289 100644 --- a/passbook/lib/templatetags/utils.py +++ b/passbook/lib/templatetags/utils.py @@ -18,19 +18,19 @@ register = template.Library() def back(context): """Return a link back (either from GET paramter or referer.""" - request = context.get('request') - url = '' - if 'HTTP_REFERER' in request.META: - url = request.META.get('HTTP_REFERER') - if 'back' in request.GET: - url = request.GET.get('back') + request = context.get("request") + url = "" + if "HTTP_REFERER" in request.META: + url = request.META.get("HTTP_REFERER") + if "back" in request.GET: + url = request.GET.get("back") if not is_url_absolute(url): return url - return '' + return "" -@register.filter('fieldtype') +@register.filter("fieldtype") def fieldtype(field): """Return classname""" # if issubclass(field.__class__, CastableModel): @@ -43,34 +43,40 @@ def fieldtype(field): @register.simple_tag(takes_context=True) def title(context, *title): """Return either just branding or title - branding""" - branding = CONFIG.y('passbook.branding', 'passbook') + branding = CONFIG.y("passbook.branding", "passbook") if not title: return branding # Include App Title in title - app = '' - if context.request.resolver_match and context.request.resolver_match.namespace != '': + app = "" + if ( + context.request.resolver_match + and context.request.resolver_match.namespace != "" + ): dj_app = None - namespace = context.request.resolver_match.namespace.split(':')[0] + namespace = context.request.resolver_match.namespace.split(":")[0] # New label (App URL Namespace == App Label) dj_app = apps.get_app_config(namespace) - title_modifier = getattr(dj_app, 'title_modifier', None) + title_modifier = getattr(dj_app, "title_modifier", None) if title_modifier: app_title = dj_app.title_modifier(context.request) - app = app_title + ' -' - return _("%(title)s - %(app)s %(branding)s" % { - 'title': ' - '.join([str(x) for x in title]), - 'branding': branding, - 'app': app, - }) + app = app_title + " -" + return _( + "%(title)s - %(app)s %(branding)s" + % { + "title": " - ".join([str(x) for x in title]), + "branding": branding, + "app": app, + } + ) @register.simple_tag -def config(path, default=''): +def config(path, default=""): """Get a setting from the database. Returns default is setting doesn't exist.""" return CONFIG.y(path, default) -@register.filter(name='css_class') +@register.filter(name="css_class") def css_class(field, css): """Add css class to form field""" return field.as_widget(attrs={"class": css}) @@ -90,16 +96,15 @@ def gravatar(email, size=None, rating=None): {% gravatar someone@example.com 48 pg %} """ # gravatar uses md5 for their URLs, so md5 can't be avoided - gravatar_url = "%savatar/%s" % ('https://secure.gravatar.com/', - md5(email.encode('utf-8')).hexdigest()) # nosec + gravatar_url = "%savatar/%s" % ( + "https://secure.gravatar.com/", + md5(email.encode("utf-8")).hexdigest(), + ) # nosec - parameters = [p for p in ( - ('s', size or '158'), - ('r', rating or 'g'), - ) if p[1]] + parameters = [p for p in (("s", size or "158"), ("r", rating or "g"),) if p[1]] if parameters: - gravatar_url += '?' + urlencode(parameters, doseq=True) + gravatar_url += "?" + urlencode(parameters, doseq=True) return escape(gravatar_url) @@ -108,7 +113,7 @@ def gravatar(email, size=None, rating=None): def verbose_name(obj): """Return Object's Verbose Name""" if not obj: - return '' + return "" return obj._meta.verbose_name @@ -116,5 +121,5 @@ def verbose_name(obj): def form_verbose_name(obj): """Return ModelForm's Object's Verbose Name""" if not obj: - return '' + return "" return obj._meta.model._meta.verbose_name diff --git a/passbook/lib/utils/http.py b/passbook/lib/utils/http.py index d5554b12d..22dc038b6 100644 --- a/passbook/lib/utils/http.py +++ b/passbook/lib/utils/http.py @@ -8,9 +8,9 @@ def _get_client_ip_from_meta(meta: Dict[str, Any]) -> Optional[str]: """Attempt to get the client's IP by checking common HTTP Headers. Returns none if no IP Could be found""" headers = ( - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_REAL_IP', - 'REMOTE_ADDR', + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", + "REMOTE_ADDR", ) for _header in headers: if _header in meta: diff --git a/passbook/lib/utils/reflection.py b/passbook/lib/utils/reflection.py index 959e439ed..59131efa0 100644 --- a/passbook/lib/utils/reflection.py +++ b/passbook/lib/utils/reflection.py @@ -4,15 +4,15 @@ from importlib import import_module def class_to_path(cls): """Turn Class (Class or instance) into module path""" - return '%s.%s' % (cls.__module__, cls.__name__) + return "%s.%s" % (cls.__module__, cls.__name__) def path_to_class(path): """Import module and return class""" if not path: return None - parts = path.split('.') - package = '.'.join(parts[:-1]) + parts = path.split(".") + package = ".".join(parts[:-1]) _class = getattr(import_module(package), parts[-1]) return _class @@ -20,11 +20,14 @@ def path_to_class(path): def get_apps(): """Get list of all passbook apps""" from django.apps.registry import apps + for _app in apps.get_app_configs(): - if _app.name.startswith('passbook'): + if _app.name.startswith("passbook"): yield _app + def app(name): """Return true if app with `name` is enabled""" from django.conf import settings + return name in settings.INSTALLED_APPS diff --git a/passbook/lib/utils/ui.py b/passbook/lib/utils/ui.py index 0c7d1b710..b04aec2d4 100644 --- a/passbook/lib/utils/ui.py +++ b/passbook/lib/utils/ui.py @@ -1,7 +1,8 @@ """passbook UI utils""" + def human_list(_list) -> str: """Convert a list of items into 'a, b or c'""" last_item = _list.pop() - result = ', '.join(_list) - return '%s or %s' % (result, last_item) + result = ", ".join(_list) + return "%s or %s" % (result, last_item) diff --git a/passbook/lib/views.py b/passbook/lib/views.py index b878b35d0..bb8dcde89 100644 --- a/passbook/lib/views.py +++ b/passbook/lib/views.py @@ -8,16 +8,18 @@ class CreateAssignPermView(CreateView): """Assign permissions to object after creation""" permissions = [ - '%s.view_%s', - '%s.change_%s', - '%s.delete_%s', + "%s.view_%s", + "%s.change_%s", + "%s.delete_%s", ] def form_valid(self, form): response = super().form_valid(form) for permission in self.permissions: full_permission = permission % ( - self.object._meta.app_label, self.object._meta.model_name) + self.object._meta.app_label, + self.object._meta.model_name, + ) print(full_permission) assign_perm(full_permission, self.request.user, self.object) return response diff --git a/passbook/policies/engine.py b/passbook/policies/engine.py index f1eee560c..e94451870 100644 --- a/passbook/policies/engine.py +++ b/passbook/policies/engine.py @@ -28,6 +28,7 @@ class PolicyProcessInfo: self.policy = policy self.result = None + class PolicyEngine: """Orchestrate policy checking, launch tasks and return result""" @@ -46,12 +47,13 @@ class PolicyEngine: def _select_subclasses(self) -> List[Policy]: """Make sure all Policies are their respective classes""" - return Policy.objects \ - .filter(pk__in=[x.pk for x in self.policies]) \ - .select_subclasses() \ - .order_by('order') + return ( + Policy.objects.filter(pk__in=[x.pk for x in self.policies]) + .select_subclasses() + .order_by("order") + ) - def build(self) -> 'PolicyEngine': + def build(self) -> "PolicyEngine": """Build task group""" cached_policies = [] for policy in self._select_subclasses(): @@ -65,8 +67,9 @@ class PolicyEngine: task = PolicyProcess(policy, self.request, task_end) LOGGER.debug("Starting Process", policy=policy) task.start() - self.__processes.append(PolicyProcessInfo(process=task, - connection=our_end, policy=policy)) + self.__processes.append( + PolicyProcessInfo(process=task, connection=our_end, policy=policy) + ) # If all policies are cached, we have an empty list here. for proc_info in self.__processes: proc_info.process.join(proc_info.policy.timeout) @@ -80,7 +83,9 @@ class PolicyEngine: """Get policy-checking result""" messages: List[str] = [] for proc_info in self.__processes: - LOGGER.debug("Result", policy=proc_info.policy, passing=proc_info.result.passing) + LOGGER.debug( + "Result", policy=proc_info.policy, passing=proc_info.result.passing + ) if proc_info.result.messages: messages += proc_info.result.messages if not proc_info.result.passing: diff --git a/passbook/policies/exceptions.py b/passbook/policies/exceptions.py index edf8d6372..5ff1f4d45 100644 --- a/passbook/policies/exceptions.py +++ b/passbook/policies/exceptions.py @@ -1,4 +1,5 @@ """policy exceptions""" + class PolicyException(Exception): """Exception that should be raised during Policy Evaluation, and can be recovered from.""" diff --git a/passbook/policies/expiry/admin.py b/passbook/policies/expiry/admin.py index 2623d7989..c9aa9e577 100644 --- a/passbook/policies/expiry/admin.py +++ b/passbook/policies/expiry/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_expiry') +admin_autoregister("passbook_policies_expiry") diff --git a/passbook/policies/expiry/api.py b/passbook/policies/expiry/api.py index 0b5ebdb7c..545411cef 100644 --- a/passbook/policies/expiry/api.py +++ b/passbook/policies/expiry/api.py @@ -11,7 +11,7 @@ class PasswordExpiryPolicySerializer(ModelSerializer): class Meta: model = PasswordExpiryPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['days', 'deny_only'] + fields = GENERAL_SERIALIZER_FIELDS + ["days", "deny_only"] class PasswordExpiryPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/expiry/apps.py b/passbook/policies/expiry/apps.py index b218da9d9..cfcff589f 100644 --- a/passbook/policies/expiry/apps.py +++ b/passbook/policies/expiry/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPolicyExpiryConfig(AppConfig): """Passbook policy_expiry app config""" - name = 'passbook.policies.expiry' - label = 'passbook_policies_expiry' - verbose_name = 'passbook Policies.Expiry' + name = "passbook.policies.expiry" + label = "passbook_policies_expiry" + verbose_name = "passbook Policies.Expiry" diff --git a/passbook/policies/expiry/forms.py b/passbook/policies/expiry/forms.py index 0ead5c233..63c9a2bf6 100644 --- a/passbook/policies/expiry/forms.py +++ b/passbook/policies/expiry/forms.py @@ -14,13 +14,11 @@ class PasswordExpiryPolicyForm(forms.ModelForm): class Meta: model = PasswordExpiryPolicy - fields = GENERAL_FIELDS + ['days', 'deny_only'] + fields = GENERAL_FIELDS + ["days", "deny_only"] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'days': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) - } - labels = { - 'deny_only': _("Only fail the policy, don't set user's password.") + "name": forms.TextInput(), + "order": forms.NumberInput(), + "days": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), } + labels = {"deny_only": _("Only fail the policy, don't set user's password.")} diff --git a/passbook/policies/expiry/migrations/0001_initial.py b/passbook/policies/expiry/migrations/0001_initial.py index 38809d9cd..a9bf5142b 100644 --- a/passbook/policies/expiry/migrations/0001_initial.py +++ b/passbook/policies/expiry/migrations/0001_initial.py @@ -9,21 +9,31 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PasswordExpiryPolicy', + name="PasswordExpiryPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('deny_only', models.BooleanField(default=False)), - ('days', models.IntegerField()), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("deny_only", models.BooleanField(default=False)), + ("days", models.IntegerField()), ], options={ - 'verbose_name': 'Password Expiry Policy', - 'verbose_name_plural': 'Password Expiry Policies', + "verbose_name": "Password Expiry Policy", + "verbose_name_plural": "Password Expiry Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/expiry/models.py b/passbook/policies/expiry/models.py index b7d3271ea..94c59a01e 100644 --- a/passbook/policies/expiry/models.py +++ b/passbook/policies/expiry/models.py @@ -19,27 +19,31 @@ class PasswordExpiryPolicy(Policy): deny_only = models.BooleanField(default=False) days = models.IntegerField() - form = 'passbook.policies.expiry.forms.PasswordExpiryPolicyForm' + form = "passbook.policies.expiry.forms.PasswordExpiryPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: """If password change date is more than x days in the past, call set_unusable_password and show a notice""" actual_days = (now() - request.user.password_change_date).days - days_since_expiry = (now() - (request.user.password_change_date + timedelta(days=self.days) - )).days + days_since_expiry = ( + now() - (request.user.password_change_date + timedelta(days=self.days)) + ).days if actual_days >= self.days: if not self.deny_only: request.user.set_unusable_password() request.user.save() - message = _(('Password expired %(days)d days ago. ' - 'Please update your password.') % { - 'days': days_since_expiry - }) + message = _( + ( + "Password expired %(days)d days ago. " + "Please update your password." + ) + % {"days": days_since_expiry} + ) return PolicyResult(False, message) - return PolicyResult(False, _('Password has expired.')) + return PolicyResult(False, _("Password has expired.")) return PolicyResult(True) class Meta: - verbose_name = _('Password Expiry Policy') - verbose_name_plural = _('Password Expiry Policies') + verbose_name = _("Password Expiry Policy") + verbose_name_plural = _("Password Expiry Policies") diff --git a/passbook/policies/forms.py b/passbook/policies/forms.py index 3c02016f5..9c9e4d865 100644 --- a/passbook/policies/forms.py +++ b/passbook/policies/forms.py @@ -1,4 +1,4 @@ """General fields""" -GENERAL_FIELDS = ['name', 'negate', 'order', 'timeout'] -GENERAL_SERIALIZER_FIELDS = ['pk', 'name', 'negate', 'order', 'timeout'] +GENERAL_FIELDS = ["name", "negate", "order", "timeout"] +GENERAL_SERIALIZER_FIELDS = ["pk", "name", "negate", "order", "timeout"] diff --git a/passbook/policies/group/admin.py b/passbook/policies/group/admin.py index 7206844ec..b62aa9be1 100644 --- a/passbook/policies/group/admin.py +++ b/passbook/policies/group/admin.py @@ -1,4 +1,4 @@ """autodiscover admin""" from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_group') +admin_autoregister("passbook_policies_group") diff --git a/passbook/policies/group/api.py b/passbook/policies/group/api.py index c91a65de2..ae71a8551 100644 --- a/passbook/policies/group/api.py +++ b/passbook/policies/group/api.py @@ -11,7 +11,7 @@ class GroupMembershipPolicySerializer(ModelSerializer): class Meta: model = GroupMembershipPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['group'] + fields = GENERAL_SERIALIZER_FIELDS + ["group"] class GroupMembershipPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/group/apps.py b/passbook/policies/group/apps.py index f27b34954..0529c5d09 100644 --- a/passbook/policies/group/apps.py +++ b/passbook/policies/group/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPoliciesGroupConfig(AppConfig): """passbook Group policy app config""" - name = 'passbook.policies.group' - label = 'passbook_policies_group' - verbose_name = 'passbook Policies.Group' + name = "passbook.policies.group" + label = "passbook_policies_group" + verbose_name = "passbook Policies.Group" diff --git a/passbook/policies/group/forms.py b/passbook/policies/group/forms.py index 5bb43c0d4..46c57a264 100644 --- a/passbook/policies/group/forms.py +++ b/passbook/policies/group/forms.py @@ -12,8 +12,10 @@ class GroupMembershipPolicyForm(forms.ModelForm): class Meta: model = GroupMembershipPolicy - fields = GENERAL_FIELDS + ['group', ] + fields = GENERAL_FIELDS + [ + "group", + ] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), + "name": forms.TextInput(), + "order": forms.NumberInput(), } diff --git a/passbook/policies/group/migrations/0001_initial.py b/passbook/policies/group/migrations/0001_initial.py index a2b255184..569dffc04 100644 --- a/passbook/policies/group/migrations/0001_initial.py +++ b/passbook/policies/group/migrations/0001_initial.py @@ -9,20 +9,36 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='GroupMembershipPolicy', + name="GroupMembershipPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Group')), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="passbook_core.Group", + ), + ), ], options={ - 'verbose_name': 'Group Membership Policy', - 'verbose_name_plural': 'Group Membership Policies', + "verbose_name": "Group Membership Policy", + "verbose_name_plural": "Group Membership Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/group/models.py b/passbook/policies/group/models.py index 149eb2c0d..40a0d3b21 100644 --- a/passbook/policies/group/models.py +++ b/passbook/policies/group/models.py @@ -11,12 +11,12 @@ class GroupMembershipPolicy(Policy): group = models.ForeignKey(Group, on_delete=models.CASCADE) - form = 'passbook.policies.group.forms.GroupMembershipPolicyForm' + form = "passbook.policies.group.forms.GroupMembershipPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists()) class Meta: - verbose_name = _('Group Membership Policy') - verbose_name_plural = _('Group Membership Policies') + verbose_name = _("Group Membership Policy") + verbose_name_plural = _("Group Membership Policies") diff --git a/passbook/policies/hibp/admin.py b/passbook/policies/hibp/admin.py index a8f568fd5..c6a064c44 100644 --- a/passbook/policies/hibp/admin.py +++ b/passbook/policies/hibp/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_hibp') +admin_autoregister("passbook_policies_hibp") diff --git a/passbook/policies/hibp/api.py b/passbook/policies/hibp/api.py index 2757d26f3..1c2034edf 100644 --- a/passbook/policies/hibp/api.py +++ b/passbook/policies/hibp/api.py @@ -11,7 +11,7 @@ class HaveIBeenPwendPolicySerializer(ModelSerializer): class Meta: model = HaveIBeenPwendPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['allowed_count'] + fields = GENERAL_SERIALIZER_FIELDS + ["allowed_count"] class HaveIBeenPwendPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/hibp/apps.py b/passbook/policies/hibp/apps.py index 090b3cbf7..24a6169c8 100644 --- a/passbook/policies/hibp/apps.py +++ b/passbook/policies/hibp/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPolicyHIBPConfig(AppConfig): """Passbook hibp app config""" - name = 'passbook.policies.hibp' - label = 'passbook_policies_hibp' - verbose_name = 'passbook Policies.HaveIBeenPwned' + name = "passbook.policies.hibp" + label = "passbook_policies_hibp" + verbose_name = "passbook Policies.HaveIBeenPwned" diff --git a/passbook/policies/hibp/forms.py b/passbook/policies/hibp/forms.py index 5557097f6..bc31196ca 100644 --- a/passbook/policies/hibp/forms.py +++ b/passbook/policies/hibp/forms.py @@ -14,9 +14,9 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm): class Meta: model = HaveIBeenPwendPolicy - fields = GENERAL_FIELDS + ['allowed_count'] + fields = GENERAL_FIELDS + ["allowed_count"] widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) + "name": forms.TextInput(), + "order": forms.NumberInput(), + "policies": FilteredSelectMultiple(_("policies"), False), } diff --git a/passbook/policies/hibp/migrations/0001_initial.py b/passbook/policies/hibp/migrations/0001_initial.py index b6069d17b..cb3f04898 100644 --- a/passbook/policies/hibp/migrations/0001_initial.py +++ b/passbook/policies/hibp/migrations/0001_initial.py @@ -9,20 +9,30 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='HaveIBeenPwendPolicy', + name="HaveIBeenPwendPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('allowed_count', models.IntegerField(default=0)), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("allowed_count", models.IntegerField(default=0)), ], options={ - 'verbose_name': 'Have I Been Pwned Policy', - 'verbose_name_plural': 'Have I Been Pwned Policies', + "verbose_name": "Have I Been Pwned Policy", + "verbose_name_plural": "Have I Been Pwned Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/hibp/models.py b/passbook/policies/hibp/models.py index b737b6b2b..56b4cdbe8 100644 --- a/passbook/policies/hibp/models.py +++ b/passbook/policies/hibp/models.py @@ -10,37 +10,40 @@ from passbook.core.models import Policy, PolicyResult, User LOGGER = get_logger() + class HaveIBeenPwendPolicy(Policy): """Check if password is on HaveIBeenPwned's list by upload the first 5 characters of the SHA1 Hash.""" allowed_count = models.IntegerField(default=0) - form = 'passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm' + form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm" def passes(self, user: User) -> PolicyResult: """Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5 characters of Password in request and checks if full hash is in response. Returns 0 if Password is not in result otherwise the count of how many times it was used.""" # Only check if password is being set - if not hasattr(user, '__password__'): + if not hasattr(user, "__password__"): return PolicyResult(True) - password = getattr(user, '__password__') - pw_hash = sha1(password.encode('utf-8')).hexdigest() # nosec - url = 'https://api.pwnedpasswords.com/range/%s' % pw_hash[:5] + password = getattr(user, "__password__") + pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec + url = "https://api.pwnedpasswords.com/range/%s" % pw_hash[:5] result = get(url).text final_count = 0 - for line in result.split('\r\n'): - full_hash, count = line.split(':') + for line in result.split("\r\n"): + full_hash, count = line.split(":") if pw_hash[5:] == full_hash.lower(): final_count = int(count) LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5]) if final_count > self.allowed_count: - message = _("Password exists on %(count)d online lists." % {'count': final_count}) + message = _( + "Password exists on %(count)d online lists." % {"count": final_count} + ) return PolicyResult(False, message) return PolicyResult(True) class Meta: - verbose_name = _('Have I Been Pwned Policy') - verbose_name_plural = _('Have I Been Pwned Policies') + verbose_name = _("Have I Been Pwned Policy") + verbose_name_plural = _("Have I Been Pwned Policies") diff --git a/passbook/policies/matcher/admin.py b/passbook/policies/matcher/admin.py index c81b555a6..793013085 100644 --- a/passbook/policies/matcher/admin.py +++ b/passbook/policies/matcher/admin.py @@ -1,4 +1,4 @@ """autodiscover admin""" from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_matcher') +admin_autoregister("passbook_policies_matcher") diff --git a/passbook/policies/matcher/api.py b/passbook/policies/matcher/api.py index c0dece4bf..b8cc2904c 100644 --- a/passbook/policies/matcher/api.py +++ b/passbook/policies/matcher/api.py @@ -11,7 +11,11 @@ class FieldMatcherPolicySerializer(ModelSerializer): class Meta: model = FieldMatcherPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['user_field', 'match_action', 'value', ] + fields = GENERAL_SERIALIZER_FIELDS + [ + "user_field", + "match_action", + "value", + ] class FieldMatcherPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/matcher/apps.py b/passbook/policies/matcher/apps.py index 8d19ddd19..e78dfae0e 100644 --- a/passbook/policies/matcher/apps.py +++ b/passbook/policies/matcher/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPoliciesMatcherConfig(AppConfig): """passbook Matcher policy app config""" - name = 'passbook.policies.matcher' - label = 'passbook_policies_matcher' - verbose_name = 'passbook Policies.Matcher' + name = "passbook.policies.matcher" + label = "passbook_policies_matcher" + verbose_name = "passbook Policies.Matcher" diff --git a/passbook/policies/matcher/forms.py b/passbook/policies/matcher/forms.py index 13b4e4f36..beaccf193 100644 --- a/passbook/policies/matcher/forms.py +++ b/passbook/policies/matcher/forms.py @@ -12,8 +12,12 @@ class FieldMatcherPolicyForm(forms.ModelForm): class Meta: model = FieldMatcherPolicy - fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ] + fields = GENERAL_FIELDS + [ + "user_field", + "match_action", + "value", + ] widgets = { - 'name': forms.TextInput(), - 'value': forms.TextInput(), + "name": forms.TextInput(), + "value": forms.TextInput(), } diff --git a/passbook/policies/matcher/migrations/0001_initial.py b/passbook/policies/matcher/migrations/0001_initial.py index c1da71ea1..2834199c8 100644 --- a/passbook/policies/matcher/migrations/0001_initial.py +++ b/passbook/policies/matcher/migrations/0001_initial.py @@ -9,22 +9,56 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='FieldMatcherPolicy', + name="FieldMatcherPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('user_field', models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')])), - ('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('contains', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)), - ('value', models.TextField()), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ( + "user_field", + models.TextField( + choices=[ + ("username", "Username"), + ("name", "Name"), + ("email", "E-Mail"), + ("is_staff", "Is staff"), + ("is_active", "Is active"), + ("data_joined", "Date joined"), + ] + ), + ), + ( + "match_action", + models.CharField( + choices=[ + ("startswith", "Starts with"), + ("endswith", "Ends with"), + ("contains", "Contains"), + ("regexp", "Regexp"), + ("exact", "Exact"), + ], + max_length=50, + ), + ), + ("value", models.TextField()), ], options={ - 'verbose_name': 'Field matcher Policy', - 'verbose_name_plural': 'Field matcher Policies', + "verbose_name": "Field matcher Policy", + "verbose_name_plural": "Field matcher Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/matcher/models.py b/passbook/policies/matcher/models.py index dec2f4b1c..63469481e 100644 --- a/passbook/policies/matcher/models.py +++ b/passbook/policies/matcher/models.py @@ -10,41 +10,44 @@ from passbook.policies.struct import PolicyRequest, PolicyResult LOGGER = get_logger() + class FieldMatcherPolicy(Policy): """Policy which checks if a field of the User model matches/doesn't match a certain pattern""" - MATCH_STARTSWITH = 'startswith' - MATCH_ENDSWITH = 'endswith' - MATCH_CONTAINS = 'contains' - MATCH_REGEXP = 'regexp' - MATCH_EXACT = 'exact' + MATCH_STARTSWITH = "startswith" + MATCH_ENDSWITH = "endswith" + MATCH_CONTAINS = "contains" + MATCH_REGEXP = "regexp" + MATCH_EXACT = "exact" MATCHES = ( - (MATCH_STARTSWITH, _('Starts with')), - (MATCH_ENDSWITH, _('Ends with')), - (MATCH_CONTAINS, _('Contains')), - (MATCH_REGEXP, _('Regexp')), - (MATCH_EXACT, _('Exact')), + (MATCH_STARTSWITH, _("Starts with")), + (MATCH_ENDSWITH, _("Ends with")), + (MATCH_CONTAINS, _("Contains")), + (MATCH_REGEXP, _("Regexp")), + (MATCH_EXACT, _("Exact")), ) USER_FIELDS = ( - ('username', _('Username'),), - ('name', _('Name'),), - ('email', _('E-Mail'),), - ('is_staff', _('Is staff'),), - ('is_active', _('Is active'),), - ('data_joined', _('Date joined'),), + ("username", _("Username"),), + ("name", _("Name"),), + ("email", _("E-Mail"),), + ("is_staff", _("Is staff"),), + ("is_active", _("Is active"),), + ("data_joined", _("Date joined"),), ) user_field = models.TextField(choices=USER_FIELDS) match_action = models.CharField(max_length=50, choices=MATCHES) value = models.TextField() - form = 'passbook.policies.matcher.forms.FieldMatcherPolicyForm' + form = "passbook.policies.matcher.forms.FieldMatcherPolicyForm" def __str__(self): - description = f"{self.name}, user.{self.user_field} {self.match_action} '{self.value}'" + description = ( + f"{self.name}, user.{self.user_field} {self.match_action} '{self.value}'" + ) if self.name: description = f"{self.name}: {description}" return description @@ -54,8 +57,12 @@ class FieldMatcherPolicy(Policy): if not hasattr(request.user, self.user_field): raise ValueError("Field does not exist") user_field_value = getattr(request.user, self.user_field, None) - LOGGER.debug("Checking field", value=user_field_value, - action=self.match_action, should_be=self.value) + LOGGER.debug( + "Checking field", + value=user_field_value, + action=self.match_action, + should_be=self.value, + ) passes = False if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH: passes = user_field_value.startswith(self.value) @@ -72,5 +79,5 @@ class FieldMatcherPolicy(Policy): class Meta: - verbose_name = _('Field matcher Policy') - verbose_name_plural = _('Field matcher Policies') + verbose_name = _("Field matcher Policy") + verbose_name_plural = _("Field matcher Policies") diff --git a/passbook/policies/password/admin.py b/passbook/policies/password/admin.py index 919eeae08..11d83aeca 100644 --- a/passbook/policies/password/admin.py +++ b/passbook/policies/password/admin.py @@ -1,4 +1,4 @@ """autodiscover admin""" from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_password') +admin_autoregister("passbook_policies_password") diff --git a/passbook/policies/password/api.py b/passbook/policies/password/api.py index deade05ab..db5e1bd58 100644 --- a/passbook/policies/password/api.py +++ b/passbook/policies/password/api.py @@ -11,9 +11,14 @@ class PasswordPolicySerializer(ModelSerializer): class Meta: model = PasswordPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['amount_uppercase', 'amount_lowercase', - 'amount_symbols', 'length_min', 'symbol_charset', - 'error_message'] + fields = GENERAL_SERIALIZER_FIELDS + [ + "amount_uppercase", + "amount_lowercase", + "amount_symbols", + "length_min", + "symbol_charset", + "error_message", + ] class PasswordPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/password/apps.py b/passbook/policies/password/apps.py index e8a3abc47..6bd6bcb36 100644 --- a/passbook/policies/password/apps.py +++ b/passbook/policies/password/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPoliciesPasswordConfig(AppConfig): """passbook Password policy app config""" - name = 'passbook.policies.password' - label = 'passbook_policies_password' - verbose_name = 'passbook Policies.Password' + name = "passbook.policies.password" + label = "passbook_policies_password" + verbose_name = "passbook Policies.Password" diff --git a/passbook/policies/password/forms.py b/passbook/policies/password/forms.py index ba505f7b3..a60fd522f 100644 --- a/passbook/policies/password/forms.py +++ b/passbook/policies/password/forms.py @@ -13,17 +13,22 @@ class PasswordPolicyForm(forms.ModelForm): class Meta: model = PasswordPolicy - fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase', - 'amount_symbols', 'length_min', 'symbol_charset', - 'error_message'] + fields = GENERAL_FIELDS + [ + "amount_uppercase", + "amount_lowercase", + "amount_symbols", + "length_min", + "symbol_charset", + "error_message", + ] widgets = { - 'name': forms.TextInput(), - 'symbol_charset': forms.TextInput(), - 'error_message': forms.TextInput(), + "name": forms.TextInput(), + "symbol_charset": forms.TextInput(), + "error_message": forms.TextInput(), } labels = { - 'amount_uppercase': _('Minimum amount of Uppercase Characters'), - 'amount_lowercase': _('Minimum amount of Lowercase Characters'), - 'amount_symbols': _('Minimum amount of Symbols Characters'), - 'length_min': _('Minimum Length'), + "amount_uppercase": _("Minimum amount of Uppercase Characters"), + "amount_lowercase": _("Minimum amount of Lowercase Characters"), + "amount_symbols": _("Minimum amount of Symbols Characters"), + "length_min": _("Minimum Length"), } diff --git a/passbook/policies/password/migrations/0001_initial.py b/passbook/policies/password/migrations/0001_initial.py index 389ed060e..ef73a1871 100644 --- a/passbook/policies/password/migrations/0001_initial.py +++ b/passbook/policies/password/migrations/0001_initial.py @@ -9,25 +9,38 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PasswordPolicy', + name="PasswordPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('amount_uppercase', models.IntegerField(default=0)), - ('amount_lowercase', models.IntegerField(default=0)), - ('amount_symbols', models.IntegerField(default=0)), - ('length_min', models.IntegerField(default=0)), - ('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')), - ('error_message', models.TextField()), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("amount_uppercase", models.IntegerField(default=0)), + ("amount_lowercase", models.IntegerField(default=0)), + ("amount_symbols", models.IntegerField(default=0)), + ("length_min", models.IntegerField(default=0)), + ( + "symbol_charset", + models.TextField(default="!\\\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ "), + ), + ("error_message", models.TextField()), ], options={ - 'verbose_name': 'Password Policy', - 'verbose_name_plural': 'Password Policies', + "verbose_name": "Password Policy", + "verbose_name_plural": "Password Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/password/models.py b/passbook/policies/password/models.py index b7f72be32..6d81d2109 100644 --- a/passbook/policies/password/models.py +++ b/passbook/policies/password/models.py @@ -21,21 +21,21 @@ class PasswordPolicy(Policy): symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") error_message = models.TextField() - form = 'passbook.policies.password.forms.PasswordPolicyForm' + form = "passbook.policies.password.forms.PasswordPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: # Only check if password is being set - if not hasattr(request.user, '__password__'): + if not hasattr(request.user, "__password__"): return PolicyResult(True) - password = getattr(request.user, '__password__') + password = getattr(request.user, "__password__") - filter_regex = r'' + filter_regex = r"" if self.amount_lowercase > 0: - filter_regex += r'[a-z]{%d,}' % self.amount_lowercase + filter_regex += r"[a-z]{%d,}" % self.amount_lowercase if self.amount_uppercase > 0: - filter_regex += r'[A-Z]{%d,}' % self.amount_uppercase + filter_regex += r"[A-Z]{%d,}" % self.amount_uppercase if self.amount_symbols > 0: - filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols) + filter_regex += r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols) result = bool(re.compile(filter_regex).match(password)) if not result: return PolicyResult(result, self.error_message) @@ -43,5 +43,5 @@ class PasswordPolicy(Policy): class Meta: - verbose_name = _('Password Policy') - verbose_name_plural = _('Password Policies') + verbose_name = _("Password Policy") + verbose_name_plural = _("Password Policies") diff --git a/passbook/policies/process.py b/passbook/policies/process.py index 14198d832..9615300cf 100644 --- a/passbook/policies/process.py +++ b/passbook/policies/process.py @@ -16,6 +16,7 @@ def cache_key(policy, user): """Generate Cache key for policy""" return f"policy_{policy.pk}#{user.pk}" + class PolicyProcess(Process): """Evaluate a single policy within a seprate process""" @@ -31,8 +32,12 @@ class PolicyProcess(Process): def run(self): """Task wrapper to run policy checking""" - LOGGER.debug("Running policy", policy=self.policy, - user=self.request.user, process="PolicyProcess") + LOGGER.debug( + "Running policy", + policy=self.policy, + user=self.request.user, + process="PolicyProcess", + ) try: policy_result = self.policy.passes(self.request) except PolicyException as exc: @@ -41,8 +46,14 @@ class PolicyProcess(Process): # Invert result if policy.negate is set if self.policy.negate: policy_result.passing = not policy_result.passing - LOGGER.debug("Got result", policy=self.policy, result=policy_result, - process="PolicyProcess", passing=policy_result.passing, user=self.request.user) + LOGGER.debug( + "Got result", + policy=self.policy, + result=policy_result, + process="PolicyProcess", + passing=policy_result.passing, + user=self.request.user, + ) key = cache_key(self.policy, self.request.user) cache.set(key, policy_result) LOGGER.debug("Cached policy evaluation", key=key) diff --git a/passbook/policies/reputation/admin.py b/passbook/policies/reputation/admin.py index 354368fc4..bfe52af9d 100644 --- a/passbook/policies/reputation/admin.py +++ b/passbook/policies/reputation/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_reputation') +admin_autoregister("passbook_policies_reputation") diff --git a/passbook/policies/reputation/api.py b/passbook/policies/reputation/api.py index bd2af471e..a0a068852 100644 --- a/passbook/policies/reputation/api.py +++ b/passbook/policies/reputation/api.py @@ -11,7 +11,7 @@ class ReputationPolicySerializer(ModelSerializer): class Meta: model = ReputationPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['check_ip', 'check_username', 'threshold'] + fields = GENERAL_SERIALIZER_FIELDS + ["check_ip", "check_username", "threshold"] class ReputationPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/reputation/apps.py b/passbook/policies/reputation/apps.py index 663b8e307..712a0bbde 100644 --- a/passbook/policies/reputation/apps.py +++ b/passbook/policies/reputation/apps.py @@ -7,9 +7,9 @@ from django.apps import AppConfig class PassbookPolicyReputationConfig(AppConfig): """Passbook reputation app config""" - name = 'passbook.policies.reputation' - label = 'passbook_policies_reputation' - verbose_name = 'passbook Policies.Reputation' + name = "passbook.policies.reputation" + label = "passbook_policies_reputation" + verbose_name = "passbook Policies.Reputation" def ready(self): - import_module('passbook.policies.reputation.signals') + import_module("passbook.policies.reputation.signals") diff --git a/passbook/policies/reputation/forms.py b/passbook/policies/reputation/forms.py index f173d234b..1b9ccd667 100644 --- a/passbook/policies/reputation/forms.py +++ b/passbook/policies/reputation/forms.py @@ -12,11 +12,11 @@ class ReputationPolicyForm(forms.ModelForm): class Meta: model = ReputationPolicy - fields = GENERAL_FIELDS + ['check_ip', 'check_username', 'threshold'] + fields = GENERAL_FIELDS + ["check_ip", "check_username", "threshold"] widgets = { - 'name': forms.TextInput(), - 'value': forms.TextInput(), + "name": forms.TextInput(), + "value": forms.TextInput(), } labels = { - 'check_ip': _('Check IP'), + "check_ip": _("Check IP"), } diff --git a/passbook/policies/reputation/migrations/0001_initial.py b/passbook/policies/reputation/migrations/0001_initial.py index ffddca276..7b9dd1c2e 100644 --- a/passbook/policies/reputation/migrations/0001_initial.py +++ b/passbook/policies/reputation/migrations/0001_initial.py @@ -10,41 +10,73 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='IPReputation', + name="IPReputation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ip', models.GenericIPAddressField(unique=True)), - ('score', models.IntegerField(default=0)), - ('updated', models.DateTimeField(auto_now=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("ip", models.GenericIPAddressField(unique=True)), + ("score", models.IntegerField(default=0)), + ("updated", models.DateTimeField(auto_now=True)), ], ), migrations.CreateModel( - name='ReputationPolicy', + name="ReputationPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('check_ip', models.BooleanField(default=True)), - ('check_username', models.BooleanField(default=True)), - ('threshold', models.IntegerField(default=-5)), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("check_ip", models.BooleanField(default=True)), + ("check_username", models.BooleanField(default=True)), + ("threshold", models.IntegerField(default=-5)), ], options={ - 'verbose_name': 'Reputation Policy', - 'verbose_name_plural': 'Reputation Policies', + "verbose_name": "Reputation Policy", + "verbose_name_plural": "Reputation Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), migrations.CreateModel( - name='UserReputation', + name="UserReputation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('score', models.IntegerField(default=0)), - ('updated', models.DateTimeField(auto_now=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.IntegerField(default=0)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/passbook/policies/reputation/models.py b/passbook/policies/reputation/models.py index fe7e79054..c1f36e0b8 100644 --- a/passbook/policies/reputation/models.py +++ b/passbook/policies/reputation/models.py @@ -14,24 +14,27 @@ class ReputationPolicy(Policy): check_username = models.BooleanField(default=True) threshold = models.IntegerField(default=-5) - form = 'passbook.policies.reputation.forms.ReputationPolicyForm' + form = "passbook.policies.reputation.forms.ReputationPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: remote_ip = get_client_ip(request.http_request) passing = True if self.check_ip: - ip_scores = IPReputation.objects.filter(ip=remote_ip, score__lte=self.threshold) + ip_scores = IPReputation.objects.filter( + ip=remote_ip, score__lte=self.threshold + ) passing = passing and ip_scores.exists() if self.check_username: - user_scores = UserReputation.objects.filter(user=request.user, - score__lte=self.threshold) + user_scores = UserReputation.objects.filter( + user=request.user, score__lte=self.threshold + ) passing = passing and user_scores.exists() return PolicyResult(passing) class Meta: - verbose_name = _('Reputation Policy') - verbose_name_plural = _('Reputation Policies') + verbose_name = _("Reputation Policy") + verbose_name_plural = _("Reputation Policies") class IPReputation(models.Model): diff --git a/passbook/policies/reputation/signals.py b/passbook/policies/reputation/signals.py index f53c7fd06..e11c38a93 100644 --- a/passbook/policies/reputation/signals.py +++ b/passbook/policies/reputation/signals.py @@ -12,7 +12,7 @@ LOGGER = get_logger() def update_score(request, username, amount): """Update score for IP and User""" - remote_ip = get_client_ip(request) or '255.255.255.255.' + remote_ip = get_client_ip(request) or "255.255.255.255." ip_score, _ = IPReputation.objects.update_or_create(ip=remote_ip) ip_score.score += amount ip_score.save() @@ -30,7 +30,7 @@ def update_score(request, username, amount): # pylint: disable=unused-argument def handle_failed_login(sender, request, credentials, **_): """Lower Score for failed loging attempts""" - update_score(request, credentials.get('username'), -1) + update_score(request, credentials.get("username"), -1) @receiver(user_logged_in) diff --git a/passbook/policies/sso/admin.py b/passbook/policies/sso/admin.py index a5146d89f..b76e6afcf 100644 --- a/passbook/policies/sso/admin.py +++ b/passbook/policies/sso/admin.py @@ -1,4 +1,4 @@ """autodiscover admin""" from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_sso') +admin_autoregister("passbook_policies_sso") diff --git a/passbook/policies/sso/apps.py b/passbook/policies/sso/apps.py index 18ae2fc53..60773e5a6 100644 --- a/passbook/policies/sso/apps.py +++ b/passbook/policies/sso/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPoliciesSSOConfig(AppConfig): """passbook sso policy app config""" - name = 'passbook.policies.sso' - label = 'passbook_policies_sso' - verbose_name = 'passbook Policies.SSO' + name = "passbook.policies.sso" + label = "passbook_policies_sso" + verbose_name = "passbook Policies.SSO" diff --git a/passbook/policies/sso/forms.py b/passbook/policies/sso/forms.py index 52fc0e50b..0503b6a97 100644 --- a/passbook/policies/sso/forms.py +++ b/passbook/policies/sso/forms.py @@ -14,6 +14,6 @@ class SSOLoginPolicyForm(forms.ModelForm): model = SSOLoginPolicy fields = GENERAL_FIELDS widgets = { - 'name': forms.TextInput(), - 'order': forms.NumberInput(), + "name": forms.TextInput(), + "order": forms.NumberInput(), } diff --git a/passbook/policies/sso/migrations/0001_initial.py b/passbook/policies/sso/migrations/0001_initial.py index 4d0a3cf7d..4b31f32b0 100644 --- a/passbook/policies/sso/migrations/0001_initial.py +++ b/passbook/policies/sso/migrations/0001_initial.py @@ -9,19 +9,29 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='SSOLoginPolicy', + name="SSOLoginPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), ], options={ - 'verbose_name': 'SSO Login Policy', - 'verbose_name_plural': 'SSO Login Policies', + "verbose_name": "SSO Login Policy", + "verbose_name_plural": "SSO Login Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/sso/models.py b/passbook/policies/sso/models.py index 560d2597c..1f45fa085 100644 --- a/passbook/policies/sso/models.py +++ b/passbook/policies/sso/models.py @@ -8,15 +8,18 @@ from passbook.policies.struct import PolicyRequest, PolicyResult class SSOLoginPolicy(Policy): """Policy that applies to users that have authenticated themselves through SSO""" - form = 'passbook.policies.sso.forms.SSOLoginPolicyForm' + form = "passbook.policies.sso.forms.SSOLoginPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: """Check if user instance passes this policy""" from passbook.factors.view import AuthenticationView - is_sso_login = request.user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False) + + is_sso_login = request.user.session.get( + AuthenticationView.SESSION_IS_SSO_LOGIN, False + ) return PolicyResult(is_sso_login) class Meta: - verbose_name = _('SSO Login Policy') - verbose_name_plural = _('SSO Login Policies') + verbose_name = _("SSO Login Policy") + verbose_name_plural = _("SSO Login Policies") diff --git a/passbook/policies/struct.py b/passbook/policies/struct.py index c6432df5a..126602690 100644 --- a/passbook/policies/struct.py +++ b/passbook/policies/struct.py @@ -9,6 +9,7 @@ from django.http import HttpRequest if TYPE_CHECKING: from passbook.core.models import User + class PolicyRequest: """Data-class to hold policy request data""" diff --git a/passbook/policies/tests/test_engine.py b/passbook/policies/tests/test_engine.py index e2007c66a..07a111ba8 100644 --- a/passbook/policies/tests/test_engine.py +++ b/passbook/policies/tests/test_engine.py @@ -11,23 +11,17 @@ class PolicyTestEngine(TestCase): def setUp(self): cache.clear() - self.user = User.objects.create_user( - username="policyuser") + self.user = User.objects.create_user(username="policyuser") self.policy_false = DebugPolicy.objects.create( - result=False, - wait_min=0, - wait_max=1) + result=False, wait_min=0, wait_max=1 + ) self.policy_true = DebugPolicy.objects.create( - result=True, - wait_min=0, - wait_max=1) + result=True, wait_min=0, wait_max=1 + ) self.policy_negate = DebugPolicy.objects.create( - negate=True, - result=True, - wait_min=0, - wait_max=1) - self.policy_raises = Policy.objects.create( - name='raises') + negate=True, result=True, wait_min=0, wait_max=1 + ) + self.policy_raises = Policy.objects.create(name="raises") def test_engine_empty(self): """Ensure empty policy list passes""" @@ -36,7 +30,9 @@ class PolicyTestEngine(TestCase): def test_engine(self): """Ensure all policies passes (Mix of false and true -> false)""" - engine = PolicyEngine(DebugPolicy.objects.filter(negate__exact=False), self.user) + engine = PolicyEngine( + DebugPolicy.objects.filter(negate__exact=False), self.user + ) self.assertEqual(engine.build().passing, False) def test_engine_negate(self): @@ -46,14 +42,16 @@ class PolicyTestEngine(TestCase): def test_engine_policy_error(self): """Test negate flag""" - engine = PolicyEngine(Policy.objects.filter(name='raises'), self.user) + engine = PolicyEngine(Policy.objects.filter(name="raises"), self.user) self.assertEqual(engine.build().passing, False) def test_engine_cache(self): """Ensure empty policy list passes""" - engine = PolicyEngine(DebugPolicy.objects.filter(negate__exact=False), self.user) - self.assertEqual(len(cache.keys('policy_*')), 0) + engine = PolicyEngine( + DebugPolicy.objects.filter(negate__exact=False), self.user + ) + self.assertEqual(len(cache.keys("policy_*")), 0) self.assertEqual(engine.build().passing, False) - self.assertEqual(len(cache.keys('policy_*')), 2) + self.assertEqual(len(cache.keys("policy_*")), 2) self.assertEqual(engine.build().passing, False) - self.assertEqual(len(cache.keys('policy_*')), 2) + self.assertEqual(len(cache.keys("policy_*")), 2) diff --git a/passbook/policies/webhook/admin.py b/passbook/policies/webhook/admin.py index cf14a6054..3fad9a10d 100644 --- a/passbook/policies/webhook/admin.py +++ b/passbook/policies/webhook/admin.py @@ -1,4 +1,4 @@ """autodiscover admin""" from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_policies_webhook') +admin_autoregister("passbook_policies_webhook") diff --git a/passbook/policies/webhook/api.py b/passbook/policies/webhook/api.py index c433d6a7e..02a84f125 100644 --- a/passbook/policies/webhook/api.py +++ b/passbook/policies/webhook/api.py @@ -11,8 +11,14 @@ class WebhookPolicySerializer(ModelSerializer): class Meta: model = WebhookPolicy - fields = GENERAL_SERIALIZER_FIELDS + ['url', 'method', 'json_body', 'json_headers', - 'result_jsonpath', 'result_json_value', ] + fields = GENERAL_SERIALIZER_FIELDS + [ + "url", + "method", + "json_body", + "json_headers", + "result_jsonpath", + "result_json_value", + ] class WebhookPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/webhook/apps.py b/passbook/policies/webhook/apps.py index 38738930d..7d0ec7b1a 100644 --- a/passbook/policies/webhook/apps.py +++ b/passbook/policies/webhook/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookPoliciesWebhookConfig(AppConfig): """passbook Webhook policy app config""" - name = 'passbook.policies.webhook' - label = 'passbook_policies_webhook' - verbose_name = 'passbook Policies.Webhook' + name = "passbook.policies.webhook" + label = "passbook_policies_webhook" + verbose_name = "passbook Policies.Webhook" diff --git a/passbook/policies/webhook/forms.py b/passbook/policies/webhook/forms.py index bbf7f1f5a..427c1700c 100644 --- a/passbook/policies/webhook/forms.py +++ b/passbook/policies/webhook/forms.py @@ -12,12 +12,18 @@ class WebhookPolicyForm(forms.ModelForm): class Meta: model = WebhookPolicy - fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers', - 'result_jsonpath', 'result_json_value', ] + fields = GENERAL_FIELDS + [ + "url", + "method", + "json_body", + "json_headers", + "result_jsonpath", + "result_json_value", + ] widgets = { - 'name': forms.TextInput(), - 'json_body': forms.TextInput(), - 'json_headers': forms.TextInput(), - 'result_jsonpath': forms.TextInput(), - 'result_json_value': forms.TextInput(), + "name": forms.TextInput(), + "json_body": forms.TextInput(), + "json_headers": forms.TextInput(), + "result_jsonpath": forms.TextInput(), + "result_json_value": forms.TextInput(), } diff --git a/passbook/policies/webhook/migrations/0001_initial.py b/passbook/policies/webhook/migrations/0001_initial.py index 871215c8f..762766264 100644 --- a/passbook/policies/webhook/migrations/0001_initial.py +++ b/passbook/policies/webhook/migrations/0001_initial.py @@ -9,25 +9,47 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='WebhookPolicy', + name="WebhookPolicy", fields=[ - ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), - ('url', models.URLField()), - ('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)), - ('json_body', models.TextField()), - ('json_headers', models.TextField()), - ('result_jsonpath', models.TextField()), - ('result_json_value', models.TextField()), + ( + "policy_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Policy", + ), + ), + ("url", models.URLField()), + ( + "method", + models.CharField( + choices=[ + ("GET", "GET"), + ("POST", "POST"), + ("PATCH", "PATCH"), + ("DELETE", "DELETE"), + ("PUT", "PUT"), + ], + max_length=10, + ), + ), + ("json_body", models.TextField()), + ("json_headers", models.TextField()), + ("result_jsonpath", models.TextField()), + ("result_json_value", models.TextField()), ], options={ - 'verbose_name': 'Webhook Policy', - 'verbose_name_plural': 'Webhook Policies', + "verbose_name": "Webhook Policy", + "verbose_name_plural": "Webhook Policies", }, - bases=('passbook_core.policy',), + bases=("passbook_core.policy",), ), ] diff --git a/passbook/policies/webhook/models.py b/passbook/policies/webhook/models.py index 91cf59608..d5487ec3f 100644 --- a/passbook/policies/webhook/models.py +++ b/passbook/policies/webhook/models.py @@ -9,11 +9,11 @@ from passbook.policies.struct import PolicyRequest, PolicyResult class WebhookPolicy(Policy): """Policy that asks webhook""" - METHOD_GET = 'GET' - METHOD_POST = 'POST' - METHOD_PATCH = 'PATCH' - METHOD_DELETE = 'DELETE' - METHOD_PUT = 'PUT' + METHOD_GET = "GET" + METHOD_POST = "POST" + METHOD_PATCH = "PATCH" + METHOD_DELETE = "DELETE" + METHOD_PUT = "PUT" METHODS = ( (METHOD_GET, METHOD_GET), @@ -30,7 +30,7 @@ class WebhookPolicy(Policy): result_jsonpath = models.TextField() result_json_value = models.TextField() - form = 'passbook.policies.webhook.forms.WebhookPolicyForm' + form = "passbook.policies.webhook.forms.WebhookPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: """Call webhook asynchronously and report back""" @@ -38,5 +38,5 @@ class WebhookPolicy(Policy): class Meta: - verbose_name = _('Webhook Policy') - verbose_name_plural = _('Webhook Policies') + verbose_name = _("Webhook Policy") + verbose_name_plural = _("Webhook Policies") diff --git a/passbook/providers/app_gw/admin.py b/passbook/providers/app_gw/admin.py index 80e51f8b4..a0fe583ea 100644 --- a/passbook/providers/app_gw/admin.py +++ b/passbook/providers/app_gw/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_providers_app_gw') +admin_autoregister("passbook_providers_app_gw") diff --git a/passbook/providers/app_gw/api.py b/passbook/providers/app_gw/api.py index ae386c58a..39ccbc4b6 100644 --- a/passbook/providers/app_gw/api.py +++ b/passbook/providers/app_gw/api.py @@ -1,6 +1,5 @@ """ApplicationGatewayProvider API Views""" -from oauth2_provider.generators import (generate_client_id, - generate_client_secret) +from oauth2_provider.generators import generate_client_id, generate_client_secret from oidc_provider.models import Client from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet @@ -17,8 +16,8 @@ class ApplicationGatewayProviderSerializer(ModelSerializer): def create(self, validated_data): instance = super().create(validated_data) instance.client = Client.objects.create( - client_id=generate_client_id(), - client_secret=generate_client_secret()) + client_id=generate_client_id(), client_secret=generate_client_secret() + ) instance.save() return instance @@ -28,15 +27,16 @@ class ApplicationGatewayProviderSerializer(ModelSerializer): f"http://{self.instance.host}/oauth2/callback", f"https://{self.instance.host}/oauth2/callback", ] - self.instance.client.scope = ['openid', 'email'] + self.instance.client.scope = ["openid", "email"] self.instance.client.save() return super().update(instance, validated_data) class Meta: model = ApplicationGatewayProvider - fields = ['pk', 'name', 'host', 'client'] - read_only_fields = ['client'] + fields = ["pk", "name", "host", "client"] + read_only_fields = ["client"] + class ApplicationGatewayProviderViewSet(ModelViewSet): """ApplicationGatewayProvider Viewset""" diff --git a/passbook/providers/app_gw/apps.py b/passbook/providers/app_gw/apps.py index 0e5c803e9..a1702dcf9 100644 --- a/passbook/providers/app_gw/apps.py +++ b/passbook/providers/app_gw/apps.py @@ -5,7 +5,7 @@ from django.apps import AppConfig class PassbookApplicationApplicationGatewayConfig(AppConfig): """passbook app_gw app""" - name = 'passbook.providers.app_gw' - label = 'passbook_providers_app_gw' - verbose_name = 'passbook Providers.Application Security Gateway' - mountpoint = 'application/gateway/' + name = "passbook.providers.app_gw" + label = "passbook_providers_app_gw" + verbose_name = "passbook Providers.Application Security Gateway" + mountpoint = "application/gateway/" diff --git a/passbook/providers/app_gw/forms.py b/passbook/providers/app_gw/forms.py index 96abe6709..6678a6bbf 100644 --- a/passbook/providers/app_gw/forms.py +++ b/passbook/providers/app_gw/forms.py @@ -1,7 +1,6 @@ """passbook Application Security Gateway Forms""" from django import forms -from oauth2_provider.generators import (generate_client_id, - generate_client_secret) +from oauth2_provider.generators import generate_client_id, generate_client_secret from oidc_provider.models import Client from passbook.providers.app_gw.models import ApplicationGatewayProvider @@ -14,24 +13,22 @@ class ApplicationGatewayProviderForm(forms.ModelForm): if not self.instance.pk: # New instance, so we create a new OIDC client with random keys self.instance.client = Client.objects.create( - client_id=generate_client_id(), - client_secret=generate_client_secret()) + client_id=generate_client_id(), client_secret=generate_client_secret() + ) self.instance.client.name = self.instance.name self.instance.client.redirect_uris = [ f"http://{self.instance.host}/oauth2/callback", f"https://{self.instance.host}/oauth2/callback", ] - self.instance.client.scope = ['openid', 'email'] + self.instance.client.scope = ["openid", "email"] self.instance.client.save() return super().save(*args, **kwargs) class Meta: model = ApplicationGatewayProvider - fields = [ - 'name', 'host' - ] + fields = ["name", "host"] widgets = { - 'name': forms.TextInput(), - 'host': forms.TextInput(), + "name": forms.TextInput(), + "host": forms.TextInput(), } diff --git a/passbook/providers/app_gw/migrations/0001_initial.py b/passbook/providers/app_gw/migrations/0001_initial.py index 3bcdb5907..4fb55e41c 100644 --- a/passbook/providers/app_gw/migrations/0001_initial.py +++ b/passbook/providers/app_gw/migrations/0001_initial.py @@ -10,41 +10,90 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='ApplicationGatewayProvider', + name="ApplicationGatewayProvider", fields=[ - ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), - ('server_name', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)), - ('upstream', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)), - ('enabled', models.BooleanField(default=True)), - ('authentication_header', models.TextField(blank=True, default='X-Remote-User')), - ('default_content_type', models.TextField(default='application/octet-stream')), - ('upstream_ssl_verification', models.BooleanField(default=True)), + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Provider", + ), + ), + ( + "server_name", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), + ( + "upstream", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), + ("enabled", models.BooleanField(default=True)), + ( + "authentication_header", + models.TextField(blank=True, default="X-Remote-User"), + ), + ( + "default_content_type", + models.TextField(default="application/octet-stream"), + ), + ("upstream_ssl_verification", models.BooleanField(default=True)), ], options={ - 'verbose_name': 'Application Gateway Provider', - 'verbose_name_plural': 'Application Gateway Providers', + "verbose_name": "Application Gateway Provider", + "verbose_name_plural": "Application Gateway Providers", }, - bases=('passbook_core.provider',), + bases=("passbook_core.provider",), ), migrations.CreateModel( - name='RewriteRule', + name="RewriteRule", fields=[ - ('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')), - ('match', models.TextField()), - ('halt', models.BooleanField(default=False)), - ('replacement', models.TextField()), - ('redirect', models.CharField(choices=[('internal', 'Internal'), (301, 'Moved Permanently'), (302, 'Found')], max_length=50)), - ('conditions', models.ManyToManyField(blank=True, to='passbook_core.Policy')), + ( + "propertymapping_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PropertyMapping", + ), + ), + ("match", models.TextField()), + ("halt", models.BooleanField(default=False)), + ("replacement", models.TextField()), + ( + "redirect", + models.CharField( + choices=[ + ("internal", "Internal"), + (301, "Moved Permanently"), + (302, "Found"), + ], + max_length=50, + ), + ), + ( + "conditions", + models.ManyToManyField(blank=True, to="passbook_core.Policy"), + ), ], options={ - 'verbose_name': 'Rewrite Rule', - 'verbose_name_plural': 'Rewrite Rules', + "verbose_name": "Rewrite Rule", + "verbose_name_plural": "Rewrite Rules", }, - bases=('passbook_core.propertymapping',), + bases=("passbook_core.propertymapping",), ), ] diff --git a/passbook/providers/app_gw/migrations/0002_auto_20191111_1703.py b/passbook/providers/app_gw/migrations/0002_auto_20191111_1703.py index 27b67bff9..2e2912336 100644 --- a/passbook/providers/app_gw/migrations/0002_auto_20191111_1703.py +++ b/passbook/providers/app_gw/migrations/0002_auto_20191111_1703.py @@ -6,23 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_core', '0005_merge_20191025_2022'), - ('passbook_providers_app_gw', '0001_initial'), + ("passbook_core", "0005_merge_20191025_2022"), + ("passbook_providers_app_gw", "0001_initial"), ] operations = [ - migrations.RemoveField( - model_name='rewriterule', - name='conditions', - ), - migrations.RemoveField( - model_name='rewriterule', - name='propertymapping_ptr', - ), - migrations.DeleteModel( - name='ApplicationGatewayProvider', - ), - migrations.DeleteModel( - name='RewriteRule', - ), + migrations.RemoveField(model_name="rewriterule", name="conditions",), + migrations.RemoveField(model_name="rewriterule", name="propertymapping_ptr",), + migrations.DeleteModel(name="ApplicationGatewayProvider",), + migrations.DeleteModel(name="RewriteRule",), ] diff --git a/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py b/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py index c79eda864..36747a09c 100644 --- a/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py +++ b/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py @@ -9,24 +9,40 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0005_merge_20191025_2022'), - ('oidc_provider', '0026_client_multiple_response_types'), - ('passbook_providers_app_gw', '0002_auto_20191111_1703'), + ("passbook_core", "0005_merge_20191025_2022"), + ("oidc_provider", "0026_client_multiple_response_types"), + ("passbook_providers_app_gw", "0002_auto_20191111_1703"), ] operations = [ migrations.CreateModel( - name='ApplicationGatewayProvider', + name="ApplicationGatewayProvider", fields=[ - ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), - ('name', models.TextField()), - ('host', models.TextField()), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client')), + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Provider", + ), + ), + ("name", models.TextField()), + ("host", models.TextField()), + ( + "client", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="oidc_provider.Client", + ), + ), ], options={ - 'verbose_name': 'Application Gateway Provider', - 'verbose_name_plural': 'Application Gateway Providers', + "verbose_name": "Application Gateway Provider", + "verbose_name_plural": "Application Gateway Providers", }, - bases=('passbook_core.provider',), + bases=("passbook_core.provider",), ), ] diff --git a/passbook/providers/app_gw/models.py b/passbook/providers/app_gw/models.py index bcd6fb990..71e0b2044 100644 --- a/passbook/providers/app_gw/models.py +++ b/passbook/providers/app_gw/models.py @@ -18,22 +18,23 @@ class ApplicationGatewayProvider(Provider): client = models.ForeignKey(Client, on_delete=models.CASCADE) - form = 'passbook.providers.app_gw.forms.ApplicationGatewayProviderForm' + form = "passbook.providers.app_gw.forms.ApplicationGatewayProviderForm" def html_setup_urls(self, request): """return template and context modal with URLs for authorize, token, openid-config, etc""" - cookie_secret = ''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(50)) - return "app_gw/setup_modal.html", { - 'provider': self, - 'cookie_secret': cookie_secret, - 'version': __version__ - } + cookie_secret = "".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(50) + ) + return ( + "app_gw/setup_modal.html", + {"provider": self, "cookie_secret": cookie_secret, "version": __version__}, + ) def __str__(self): return f"Application Gateway {self.name}" class Meta: - verbose_name = _('Application Gateway Provider') - verbose_name_plural = _('Application Gateway Providers') + verbose_name = _("Application Gateway Provider") + verbose_name_plural = _("Application Gateway Providers") diff --git a/passbook/providers/app_gw/urls.py b/passbook/providers/app_gw/urls.py index 8546b838b..c0e58a648 100644 --- a/passbook/providers/app_gw/urls.py +++ b/passbook/providers/app_gw/urls.py @@ -4,5 +4,7 @@ from django.urls import path from passbook.providers.app_gw.views import K8sManifestView urlpatterns = [ - path('/k8s-manifest/', K8sManifestView.as_view(), name='k8s-manifest'), + path( + "/k8s-manifest/", K8sManifestView.as_view(), name="k8s-manifest" + ), ] diff --git a/passbook/providers/app_gw/views.py b/passbook/providers/app_gw/views.py index 6cfee4236..6ec35b788 100644 --- a/passbook/providers/app_gw/views.py +++ b/passbook/providers/app_gw/views.py @@ -11,13 +11,15 @@ from structlog import get_logger from passbook import __version__ from passbook.providers.app_gw.models import ApplicationGatewayProvider -ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL' +ORIGINAL_URL = "HTTP_X_ORIGINAL_URL" LOGGER = get_logger() + def get_cookie_secret(): """Generate random 50-character string for cookie-secret""" - return ''.join(SystemRandom().choice( - string.ascii_uppercase + string.digits) for _ in range(50)) + return "".join( + SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(50) + ) class K8sManifestView(LoginRequiredMixin, View): @@ -26,8 +28,13 @@ class K8sManifestView(LoginRequiredMixin, View): def get(self, request: HttpRequest, provider: int) -> HttpResponse: """Render deployment template""" provider = get_object_or_404(ApplicationGatewayProvider, pk=provider) - return render(request, 'app_gw/k8s-manifest.yaml', { - 'provider': provider, - 'cookie_secret': get_cookie_secret(), - 'version': __version__ - }, content_type='text/yaml') + return render( + request, + "app_gw/k8s-manifest.yaml", + { + "provider": provider, + "cookie_secret": get_cookie_secret(), + "version": __version__, + }, + content_type="text/yaml", + ) diff --git a/passbook/providers/oauth/api.py b/passbook/providers/oauth/api.py index 751f767b9..9647b88d3 100644 --- a/passbook/providers/oauth/api.py +++ b/passbook/providers/oauth/api.py @@ -11,8 +11,16 @@ class OAuth2ProviderSerializer(ModelSerializer): class Meta: model = OAuth2Provider - fields = ['pk', 'name', 'redirect_uris', 'client_type', - 'authorization_grant_type', 'client_id', 'client_secret', ] + fields = [ + "pk", + "name", + "redirect_uris", + "client_type", + "authorization_grant_type", + "client_id", + "client_secret", + ] + class OAuth2ProviderViewSet(ModelViewSet): """OAuth2Provider Viewset""" diff --git a/passbook/providers/oauth/apps.py b/passbook/providers/oauth/apps.py index d1e65a02e..b3f6ccdda 100644 --- a/passbook/providers/oauth/apps.py +++ b/passbook/providers/oauth/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig class PassbookProviderOAuthConfig(AppConfig): """passbook auth oauth provider app config""" - name = 'passbook.providers.oauth' - label = 'passbook_providers_oauth' - verbose_name = 'passbook Providers.OAuth' - mountpoint = '' + name = "passbook.providers.oauth" + label = "passbook_providers_oauth" + verbose_name = "passbook Providers.OAuth" + mountpoint = "" diff --git a/passbook/providers/oauth/forms.py b/passbook/providers/oauth/forms.py index dffca09d5..9c4acc4e0 100644 --- a/passbook/providers/oauth/forms.py +++ b/passbook/providers/oauth/forms.py @@ -11,5 +11,11 @@ class OAuth2ProviderForm(forms.ModelForm): class Meta: model = OAuth2Provider - fields = ['name', 'redirect_uris', 'client_type', - 'authorization_grant_type', 'client_id', 'client_secret', ] + fields = [ + "name", + "redirect_uris", + "client_type", + "authorization_grant_type", + "client_id", + "client_secret", + ] diff --git a/passbook/providers/oauth/migrations/0001_initial.py b/passbook/providers/oauth/migrations/0001_initial.py index 98174e732..8ab8224ac 100644 --- a/passbook/providers/oauth/migrations/0001_initial.py +++ b/passbook/providers/oauth/migrations/0001_initial.py @@ -11,34 +11,94 @@ class Migration(migrations.Migration): initial = True run_before = [ - ('oauth2_provider', '0001_initial'), + ("oauth2_provider", "0001_initial"), ] dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='OAuth2Provider', + name="OAuth2Provider", fields=[ - ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), - ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)), - ('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')), - ('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)), - ('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')], max_length=32)), - ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)), - ('name', models.CharField(blank=True, max_length=255)), - ('skip_authorization', models.BooleanField(default=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='passbook_providers_oauth_oauth2provider', to=settings.AUTH_USER_MODEL)), + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Provider", + ), + ), + ( + "client_id", + models.CharField( + db_index=True, + default=oauth2_provider.generators.generate_client_id, + max_length=100, + unique=True, + ), + ), + ( + "redirect_uris", + models.TextField( + blank=True, help_text="Allowed URIs list, space separated" + ), + ), + ( + "client_type", + models.CharField( + choices=[ + ("confidential", "Confidential"), + ("public", "Public"), + ], + max_length=32, + ), + ), + ( + "authorization_grant_type", + models.CharField( + choices=[ + ("authorization-code", "Authorization code"), + ("implicit", "Implicit"), + ("password", "Resource owner password-based"), + ("client-credentials", "Client credentials"), + ], + max_length=32, + ), + ), + ( + "client_secret", + models.CharField( + blank=True, + db_index=True, + default=oauth2_provider.generators.generate_client_secret, + max_length=255, + ), + ), + ("name", models.CharField(blank=True, max_length=255)), + ("skip_authorization", models.BooleanField(default=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="passbook_providers_oauth_oauth2provider", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'OAuth2 Provider', - 'verbose_name_plural': 'OAuth2 Providers', + "verbose_name": "OAuth2 Provider", + "verbose_name_plural": "OAuth2 Providers", }, - bases=('passbook_core.provider', models.Model), + bases=("passbook_core.provider", models.Model), ), ] diff --git a/passbook/providers/oauth/models.py b/passbook/providers/oauth/models.py index 9f148e57f..82426b958 100644 --- a/passbook/providers/oauth/models.py +++ b/passbook/providers/oauth/models.py @@ -10,24 +10,30 @@ from passbook.core.models import Provider class OAuth2Provider(Provider, AbstractApplication): """Associate an OAuth2 Application with a Product""" - form = 'passbook.providers.oauth.forms.OAuth2ProviderForm' + form = "passbook.providers.oauth.forms.OAuth2ProviderForm" def __str__(self): return f"OAuth2 Provider {self.name}" def html_setup_urls(self, request): """return template and context modal with URLs for authorize, token, openid-config, etc""" - return "oauth2_provider/setup_url_modal.html", { - 'provider': self, - 'authorize_url': request.build_absolute_uri( - reverse('passbook_providers_oauth:oauth2-authorize')), - 'token_url': request.build_absolute_uri( - reverse('passbook_providers_oauth:token')), - 'userinfo_url': request.build_absolute_uri( - reverse('passbook_api:openid')), - } + return ( + "oauth2_provider/setup_url_modal.html", + { + "provider": self, + "authorize_url": request.build_absolute_uri( + reverse("passbook_providers_oauth:oauth2-authorize") + ), + "token_url": request.build_absolute_uri( + reverse("passbook_providers_oauth:token") + ), + "userinfo_url": request.build_absolute_uri( + reverse("passbook_api:openid") + ), + }, + ) class Meta: - verbose_name = _('OAuth2 Provider') - verbose_name_plural = _('OAuth2 Providers') + verbose_name = _("OAuth2 Provider") + verbose_name_plural = _("OAuth2 Providers") diff --git a/passbook/providers/oauth/settings.py b/passbook/providers/oauth/settings.py index f8b077bc1..88903380b 100644 --- a/passbook/providers/oauth/settings.py +++ b/passbook/providers/oauth/settings.py @@ -1,30 +1,30 @@ """passbook OAuth_Provider""" CORS_ORIGIN_ALLOW_ALL = True -REQUEST_APPROVAL_PROMPT = 'auto' +REQUEST_APPROVAL_PROMPT = "auto" INSTALLED_APPS = [ - 'oauth2_provider', - 'corsheaders', + "oauth2_provider", + "corsheaders", ] MIDDLEWARE = [ - 'oauth2_provider.middleware.OAuth2TokenMiddleware', - 'corsheaders.middleware.CorsMiddleware', + "oauth2_provider.middleware.OAuth2TokenMiddleware", + "corsheaders.middleware.CorsMiddleware", ] AUTHENTICATION_BACKENDS = [ - 'oauth2_provider.backends.OAuth2Backend', + "oauth2_provider.backends.OAuth2Backend", ] -OAUTH2_PROVIDER_APPLICATION_MODEL = 'passbook_providers_oauth.OAuth2Provider' +OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_providers_oauth.OAuth2Provider" OAUTH2_PROVIDER = { # this is the list of available scopes - 'SCOPES': { - 'openid': 'Access OpenID Userinfo', - 'openid:userinfo': 'Access OpenID Userinfo', + "SCOPES": { + "openid": "Access OpenID Userinfo", + "openid:userinfo": "Access OpenID Userinfo", # 'write': 'Write scope', # 'groups': 'Access to your groups', - 'user:email': 'GitHub Compatibility: User E-Mail', - 'read:org': 'GitHub Compatibility: User Groups', + "user:email": "GitHub Compatibility: User E-Mail", + "read:org": "GitHub Compatibility: User Groups", } } diff --git a/passbook/providers/oauth/urls.py b/passbook/providers/oauth/urls.py index 2f64cf3d0..2c2a9e598 100644 --- a/passbook/providers/oauth/urls.py +++ b/passbook/providers/oauth/urls.py @@ -7,12 +7,21 @@ from passbook.providers.oauth.views import github, oauth2 oauth_urlpatterns = [ # Custom OAuth 2 Authorize View - path('authorize/', oauth2.PassbookAuthorizationLoadingView.as_view(), - name="oauth2-authorize"), - path('authorize/permission_ok/', oauth2.PassbookAuthorizationView.as_view(), - name="oauth2-ok-authorize"), - path('authorize/permission_denied/', oauth2.OAuthPermissionDenied.as_view(), - name='oauth2-permission-denied'), + path( + "authorize/", + oauth2.PassbookAuthorizationLoadingView.as_view(), + name="oauth2-authorize", + ), + path( + "authorize/permission_ok/", + oauth2.PassbookAuthorizationView.as_view(), + name="oauth2-ok-authorize", + ), + path( + "authorize/permission_denied/", + oauth2.OAuthPermissionDenied.as_view(), + name="oauth2-permission-denied", + ), # OAuth API path("token/", views.TokenView.as_view(), name="token"), path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"), @@ -20,13 +29,20 @@ oauth_urlpatterns = [ ] github_urlpatterns = [ - path('login/oauth/authorize', - oauth2.PassbookAuthorizationView.as_view(), name='github-authorize'), - path('login/oauth/access_token', views.TokenView.as_view(), name='github-access-token'), - path('user', github.GitHubUserView.as_view(), name='github-user'), + path( + "login/oauth/authorize", + oauth2.PassbookAuthorizationView.as_view(), + name="github-authorize", + ), + path( + "login/oauth/access_token", + views.TokenView.as_view(), + name="github-access-token", + ), + path("user", github.GitHubUserView.as_view(), name="github-user"), ] urlpatterns = [ - path('', include(github_urlpatterns)), - path('application/oauth/', include(oauth_urlpatterns)), + path("", include(github_urlpatterns)), + path("application/oauth/", include(oauth_urlpatterns)), ] diff --git a/passbook/providers/oauth/views/github.py b/passbook/providers/oauth/views/github.py index 2623bd2b4..b52446ee9 100644 --- a/passbook/providers/oauth/views/github.py +++ b/passbook/providers/oauth/views/github.py @@ -10,54 +10,58 @@ class GitHubUserView(View): def verify_access_token(self): """Verify access token manually since github uses /user?access_token=...""" - token = get_object_or_404(AccessToken, token=self.request.GET.get('access_token', '')) + token = get_object_or_404( + AccessToken, token=self.request.GET.get("access_token", "") + ) return token.user def get(self, request): """Emulate GitHub's /user API Endpoint""" user = self.verify_access_token() - return JsonResponse({ - "login": user.username, - "id": user.pk, - "node_id": "", - "avatar_url": "", - "gravatar_id": "", - "url": "", - "html_url": "", - "followers_url": "", - "following_url": "", - "gists_url": "", - "starred_url": "", - "subscriptions_url": "", - "organizations_url": "", - "repos_url": "", - "events_url": "", - "received_events_url": "", - "type": "User", - "site_admin": False, - "name": user.name, - "company": "", - "blog": "", - "location": "", - "email": user.email, - "hireable": False, - "bio": "", - "public_repos": 0, - "public_gists": 0, - "followers": 0, - "following": 0, - "created_at": user.date_joined, - "updated_at": user.date_joined, - "private_gists": 0, - "total_private_repos": 0, - "owned_private_repos": 0, - "disk_usage": 0, - "collaborators": 0, - "two_factor_authentication": True, - "plan": { - "name": "None", - "space": 0, - "private_repos": 0, - "collaborators": 0 + return JsonResponse( + { + "login": user.username, + "id": user.pk, + "node_id": "", + "avatar_url": "", + "gravatar_id": "", + "url": "", + "html_url": "", + "followers_url": "", + "following_url": "", + "gists_url": "", + "starred_url": "", + "subscriptions_url": "", + "organizations_url": "", + "repos_url": "", + "events_url": "", + "received_events_url": "", + "type": "User", + "site_admin": False, + "name": user.name, + "company": "", + "blog": "", + "location": "", + "email": user.email, + "hireable": False, + "bio": "", + "public_repos": 0, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": user.date_joined, + "updated_at": user.date_joined, + "private_gists": 0, + "total_private_repos": 0, + "owned_private_repos": 0, + "disk_usage": 0, + "collaborators": 0, + "two_factor_authentication": True, + "plan": { + "name": "None", + "space": 0, + "private_repos": 0, + "collaborators": 0, + }, } - }) + ) diff --git a/passbook/providers/oauth/views/oauth2.py b/passbook/providers/oauth/views/oauth2.py index 13ffd5ae5..fc9a83256 100644 --- a/passbook/providers/oauth/views/oauth2.py +++ b/passbook/providers/oauth/views/oauth2.py @@ -20,11 +20,13 @@ LOGGER = get_logger() class PassbookAuthorizationLoadingView(LoginRequiredMixin, LoadingView): """Show loading view for permission checks""" - title = _('Checking permissions...') + title = _("Checking permissions...") def get_url(self): querystring = urlencode(self.request.GET) - return reverse('passbook_providers_oauth:oauth2-ok-authorize') + '?' + querystring + return ( + reverse("passbook_providers_oauth:oauth2-ok-authorize") + "?" + querystring + ) class OAuthPermissionDenied(PermissionDeniedView): @@ -40,18 +42,20 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView): """Inject response_type into querystring if not set""" LOGGER.debug("response_type not set, defaulting to 'code'") querystring = urlencode(self.request.GET) - querystring += '&response_type=code' - return redirect(reverse('passbook_providers_oauth:oauth2-ok-authorize') + '?' + querystring) + querystring += "&response_type=code" + return redirect( + reverse("passbook_providers_oauth:oauth2-ok-authorize") + "?" + querystring + ) def dispatch(self, request, *args, **kwargs): """Update OAuth2Provider's skip_authorization state""" # Get client_id to get provider, so we can update skip_authorization field - client_id = request.GET.get('client_id') + client_id = request.GET.get("client_id") provider = get_object_or_404(OAuth2Provider, client_id=client_id) try: application = self.provider_to_application(provider) except Application.DoesNotExist: - return redirect('passbook_providers_oauth:oauth2-permission-denied') + return redirect("passbook_providers_oauth:oauth2-permission-denied") # Update field here so oauth-toolkit does work for us provider.skip_authorization = application.skip_authorization provider.save() @@ -61,24 +65,29 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView): if not passing: for policy_message in policy_messages: messages.error(request, policy_message) - return redirect('passbook_providers_oauth:oauth2-permission-denied') + return redirect("passbook_providers_oauth:oauth2-permission-denied") # Some clients don't pass response_type, so we default to code - if 'response_type' not in request.GET: + if "response_type" not in request.GET: return self._inject_response_type() actual_response = super().dispatch(request, *args, **kwargs) if actual_response.status_code == 400: - LOGGER.debug(request.GET.get('redirect_uri')) + LOGGER.debug(request.GET.get("redirect_uri")) return actual_response def render_to_response(self, context, **kwargs): # Always set is_login to true for correct css class - context['is_login'] = True + context["is_login"] = True return super().render_to_response(context, **kwargs) def form_valid(self, form): # User has clicked on "Authorize" - Event.new(EventAction.AUTHORIZE_APPLICATION, - authorized_application=self._application.pk).from_http(self.request) - LOGGER.debug('User authorized Application', - user=self.request.user, application=self._application) + Event.new( + EventAction.AUTHORIZE_APPLICATION, + authorized_application=self._application.pk, + ).from_http(self.request) + LOGGER.debug( + "User authorized Application", + user=self.request.user, + application=self._application, + ) return super().form_valid(form) diff --git a/passbook/providers/oidc/admin.py b/passbook/providers/oidc/admin.py index 0da5dad89..338db4948 100644 --- a/passbook/providers/oidc/admin.py +++ b/passbook/providers/oidc/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_providers_oidc') +admin_autoregister("passbook_providers_oidc") diff --git a/passbook/providers/oidc/api.py b/passbook/providers/oidc/api.py index 410c0cf8f..b2d96df01 100644 --- a/passbook/providers/oidc/api.py +++ b/passbook/providers/oidc/api.py @@ -12,8 +12,20 @@ class OpenIDProviderSerializer(ModelSerializer): class Meta: model = Client - fields = ['pk', 'name', 'client_type', 'client_id', 'client_secret', 'response_types', - 'jwt_alg', 'reuse_consent', 'require_consent', '_redirect_uris', '_scope'] + fields = [ + "pk", + "name", + "client_type", + "client_id", + "client_secret", + "response_types", + "jwt_alg", + "reuse_consent", + "require_consent", + "_redirect_uris", + "_scope", + ] + class OpenIDProviderViewSet(ModelViewSet): """OpenIDProvider Viewset""" diff --git a/passbook/providers/oidc/apps.py b/passbook/providers/oidc/apps.py index 0a8466b36..de1ecb0e6 100644 --- a/passbook/providers/oidc/apps.py +++ b/passbook/providers/oidc/apps.py @@ -6,25 +6,31 @@ from structlog import get_logger LOGGER = get_logger() + class PassbookProviderOIDCConfig(AppConfig): """passbook auth oidc provider app config""" - name = 'passbook.providers.oidc' - label = 'passbook_providers_oidc' - verbose_name = 'passbook Providers.OIDC' + name = "passbook.providers.oidc" + label = "passbook_providers_oidc" + verbose_name = "passbook Providers.OIDC" def ready(self): try: from Cryptodome.PublicKey import RSA from oidc_provider.models import RSAKey + if not RSAKey.objects.exists(): key = RSA.generate(2048) - rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8')) + rsakey = RSAKey(key=key.exportKey("PEM").decode("utf8")) rsakey.save() LOGGER.info("Created key") except (OperationalError, ProgrammingError, InternalError): pass from passbook.root import urls + urls.urlpatterns.append( - path('application/oidc/', include('oidc_provider.urls', namespace='oidc_provider')), + path( + "application/oidc/", + include("oidc_provider.urls", namespace="oidc_provider"), + ), ) diff --git a/passbook/providers/oidc/forms.py b/passbook/providers/oidc/forms.py index c8c551f16..5db052003 100644 --- a/passbook/providers/oidc/forms.py +++ b/passbook/providers/oidc/forms.py @@ -1,8 +1,7 @@ """passbook OIDC IDP Forms""" from django import forms -from oauth2_provider.generators import (generate_client_id, - generate_client_secret) +from oauth2_provider.generators import generate_client_id, generate_client_secret from oidc_provider.models import Client from passbook.providers.oidc.models import OpenIDProvider @@ -13,11 +12,11 @@ class OIDCProviderForm(forms.ModelForm): def __init__(self, *args, **kwargs): # Correctly load data from 1:1 rel - if 'instance' in kwargs and kwargs['instance']: - kwargs['instance'] = kwargs['instance'].oidc_client + if "instance" in kwargs and kwargs["instance"]: + kwargs["instance"] = kwargs["instance"].oidc_client super().__init__(*args, **kwargs) - self.fields['client_id'].initial = generate_client_id() - self.fields['client_secret'].initial = generate_client_secret() + self.fields["client_id"].initial = generate_client_id() + self.fields["client_secret"].initial = generate_client_secret() def save(self, *args, **kwargs): response = super().save(*args, **kwargs) @@ -29,9 +28,13 @@ class OIDCProviderForm(forms.ModelForm): class Meta: model = Client fields = [ - 'name', 'client_type', 'client_id', 'client_secret', 'response_types', - 'jwt_alg', '_redirect_uris', '_scope' + "name", + "client_type", + "client_id", + "client_secret", + "response_types", + "jwt_alg", + "_redirect_uris", + "_scope", ] - labels = { - 'client_secret': "Client Secret" - } + labels = {"client_secret": "Client Secret"} diff --git a/passbook/providers/oidc/lib.py b/passbook/providers/oidc/lib.py index d84fe7187..62e2f5978 100644 --- a/passbook/providers/oidc/lib.py +++ b/passbook/providers/oidc/lib.py @@ -17,8 +17,10 @@ def check_permissions(request, user, client): try: application = client.openidprovider.application except Application.DoesNotExist: - return redirect('passbook_providers_oauth:oauth2-permission-denied') - LOGGER.debug("Checking permissions for application", user=user, application=application) + return redirect("passbook_providers_oauth:oauth2-permission-denied") + LOGGER.debug( + "Checking permissions for application", user=user, application=application + ) policy_engine = PolicyEngine(application.policies.all(), user, request) policy_engine.build() @@ -27,9 +29,11 @@ def check_permissions(request, user, client): if not passing: for policy_message in policy_messages: messages.error(request, policy_message) - return redirect('passbook_providers_oauth:oauth2-permission-denied') + return redirect("passbook_providers_oauth:oauth2-permission-denied") - Event.new(EventAction.AUTHORIZE_APPLICATION, - authorized_application=application.pk, - skipped_authorization=False).from_http(request) + Event.new( + EventAction.AUTHORIZE_APPLICATION, + authorized_application=application.pk, + skipped_authorization=False, + ).from_http(request) return None diff --git a/passbook/providers/oidc/migrations/0001_initial.py b/passbook/providers/oidc/migrations/0001_initial.py index c9924dce3..b6d67d5f2 100644 --- a/passbook/providers/oidc/migrations/0001_initial.py +++ b/passbook/providers/oidc/migrations/0001_initial.py @@ -9,21 +9,37 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), - ('oidc_provider', '0026_client_multiple_response_types'), + ("passbook_core", "0001_initial"), + ("oidc_provider", "0026_client_multiple_response_types"), ] operations = [ migrations.CreateModel( - name='OpenIDProvider', + name="OpenIDProvider", fields=[ - ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), - ('oidc_client', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client')), + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Provider", + ), + ), + ( + "oidc_client", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="oidc_provider.Client", + ), + ), ], options={ - 'verbose_name': 'OpenID Provider', - 'verbose_name_plural': 'OpenID Providers', + "verbose_name": "OpenID Provider", + "verbose_name_plural": "OpenID Providers", }, - bases=('passbook_core.provider',), + bases=("passbook_core.provider",), ), ] diff --git a/passbook/providers/oidc/models.py b/passbook/providers/oidc/models.py index 9dea3dc4a..b22f33b5b 100644 --- a/passbook/providers/oidc/models.py +++ b/passbook/providers/oidc/models.py @@ -9,13 +9,14 @@ from passbook.core.models import Provider class OpenIDProvider(Provider): """Proxy model for OIDC Client""" + # Since oidc_provider doesn't currently support swappable models # (https://github.com/juanifioren/django-oidc-provider/pull/305) # we have a 1:1 relationship, and update oidc_client when the form is saved. oidc_client = models.OneToOneField(Client, on_delete=models.CASCADE) - form = 'passbook.providers.oidc.forms.OIDCProviderForm' + form = "passbook.providers.oidc.forms.OIDCProviderForm" @property def name(self): @@ -27,19 +28,24 @@ class OpenIDProvider(Provider): def html_setup_urls(self, request): """return template and context modal with URLs for authorize, token, openid-config, etc""" - return "oidc_provider/setup_url_modal.html", { - 'provider': self, - 'authorize': request.build_absolute_uri( - reverse('oidc_provider:authorize')), - 'token': request.build_absolute_uri( - reverse('oidc_provider:token')), - 'userinfo': request.build_absolute_uri( - reverse('oidc_provider:userinfo')), - 'provider_info': request.build_absolute_uri( - reverse('oidc_provider:provider-info')), - } + return ( + "oidc_provider/setup_url_modal.html", + { + "provider": self, + "authorize": request.build_absolute_uri( + reverse("oidc_provider:authorize") + ), + "token": request.build_absolute_uri(reverse("oidc_provider:token")), + "userinfo": request.build_absolute_uri( + reverse("oidc_provider:userinfo") + ), + "provider_info": request.build_absolute_uri( + reverse("oidc_provider:provider-info") + ), + }, + ) class Meta: - verbose_name = _('OpenID Provider') - verbose_name_plural = _('OpenID Providers') + verbose_name = _("OpenID Provider") + verbose_name_plural = _("OpenID Providers") diff --git a/passbook/providers/oidc/settings.py b/passbook/providers/oidc/settings.py index 2a20f3baa..7d22fff0b 100644 --- a/passbook/providers/oidc/settings.py +++ b/passbook/providers/oidc/settings.py @@ -1,7 +1,7 @@ """passbook OIDC Provider""" INSTALLED_APPS = [ - 'oidc_provider', + "oidc_provider", ] OIDC_AFTER_USERLOGIN_HOOK = "passbook.providers.oidc.lib.check_permissions" diff --git a/passbook/providers/saml/admin.py b/passbook/providers/saml/admin.py index 8813bd64b..713248957 100644 --- a/passbook/providers/saml/admin.py +++ b/passbook/providers/saml/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_providers_saml') +admin_autoregister("passbook_providers_saml") diff --git a/passbook/providers/saml/api.py b/passbook/providers/saml/api.py index 1aa104a4f..38dc57545 100644 --- a/passbook/providers/saml/api.py +++ b/passbook/providers/saml/api.py @@ -11,8 +11,19 @@ class SAMLProviderSerializer(ModelSerializer): class Meta: model = SAMLProvider - fields = ['pk', 'name', 'property_mappings', 'acs_url', 'audience', 'processor_path', - 'issuer', 'assertion_valid_for', 'signing', 'signing_cert', 'signing_key', ] + fields = [ + "pk", + "name", + "property_mappings", + "acs_url", + "audience", + "processor_path", + "issuer", + "assertion_valid_for", + "signing", + "signing_cert", + "signing_key", + ] class SAMLProviderViewSet(ModelViewSet): @@ -28,7 +39,7 @@ class SAMLPropertyMappingSerializer(ModelSerializer): class Meta: model = SAMLPropertyMapping - fields = ['pk', 'name', 'saml_name', 'friendly_name', 'values'] + fields = ["pk", "name", "saml_name", "friendly_name", "values"] class SAMLPropertyMappingViewSet(ModelViewSet): diff --git a/passbook/providers/saml/apps.py b/passbook/providers/saml/apps.py index 8aeb55316..055d748d7 100644 --- a/passbook/providers/saml/apps.py +++ b/passbook/providers/saml/apps.py @@ -7,13 +7,14 @@ from structlog import get_logger LOGGER = get_logger() + class PassbookProviderSAMLConfig(AppConfig): """passbook saml_idp app config""" - name = 'passbook.providers.saml' - label = 'passbook_providers_saml' - verbose_name = 'passbook Providers.SAML' - mountpoint = 'application/saml/' + name = "passbook.providers.saml" + label = "passbook_providers_saml" + verbose_name = "passbook Providers.SAML" + mountpoint = "application/saml/" def ready(self): """Load source_types from config file""" diff --git a/passbook/providers/saml/base.py b/passbook/providers/saml/base.py index 84178f4d1..667834425 100644 --- a/passbook/providers/saml/base.py +++ b/passbook/providers/saml/base.py @@ -15,7 +15,7 @@ HOURS = 60 * MINUTES def get_random_id(): """Random hex id""" # It is very important that these random IDs NOT start with a number. - random_id = '_' + uuid.uuid4().hex + random_id = "_" + uuid.uuid4().hex return random_id @@ -35,7 +35,7 @@ class Processor: is_idp_initiated = False - _audience = '' + _audience = "" _assertion_params = None _assertion_xml = None _assertion_id = None @@ -52,22 +52,22 @@ class Processor: _saml_response = None _session_index = None _subject = None - _subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + _subject_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" _system_params = {} @property def dotted_path(self): """Return a dotted path to this class""" - return '{module}.{class_name}'.format( - module=self.__module__, - class_name=self.__class__.__name__) + return "{module}.{class_name}".format( + module=self.__module__, class_name=self.__class__.__name__ + ) def __init__(self, remote): self.name = remote.name self._remote = remote self._logger = get_logger() - self._system_params['ISSUER'] = self._remote.issuer - self._logger.debug('processor configured') + self._system_params["ISSUER"] = self._remote.issuer + self._logger.debug("processor configured") def _build_assertion(self): """Builds _assertion_params.""" @@ -77,18 +77,18 @@ class Processor: self._determine_session_index() self._assertion_params = { - 'ASSERTION_ID': self._assertion_id, - 'ASSERTION_SIGNATURE': '', # it's unsigned - 'AUDIENCE': self._audience, - 'AUTH_INSTANT': get_time_string(), - 'ISSUE_INSTANT': get_time_string(), - 'NOT_BEFORE': get_time_string(-1 * HOURS), # TODO: Make these settings. - 'NOT_ON_OR_AFTER': get_time_string(86400 * MINUTES), - 'SESSION_INDEX': self._session_index, - 'SESSION_NOT_ON_OR_AFTER': get_time_string(8 * HOURS), - 'SP_NAME_QUALIFIER': self._audience, - 'SUBJECT': self._subject, - 'SUBJECT_FORMAT': self._subject_format, + "ASSERTION_ID": self._assertion_id, + "ASSERTION_SIGNATURE": "", # it's unsigned + "AUDIENCE": self._audience, + "AUTH_INSTANT": get_time_string(), + "ISSUE_INSTANT": get_time_string(), + "NOT_BEFORE": get_time_string(-1 * HOURS), # TODO: Make these settings. + "NOT_ON_OR_AFTER": get_time_string(86400 * MINUTES), + "SESSION_INDEX": self._session_index, + "SESSION_NOT_ON_OR_AFTER": get_time_string(8 * HOURS), + "SP_NAME_QUALIFIER": self._audience, + "SUBJECT": self._subject, + "SUBJECT_FORMAT": self._subject_format, } self._assertion_params.update(self._system_params) self._assertion_params.update(self._request_params) @@ -97,10 +97,10 @@ class Processor: """Builds _response_params.""" self._determine_response_id() self._response_params = { - 'ASSERTION': self._assertion_xml, - 'ISSUE_INSTANT': get_time_string(), - 'RESPONSE_ID': self._response_id, - 'RESPONSE_SIGNATURE': '', # initially unsigned + "ASSERTION": self._assertion_xml, + "ISSUE_INSTANT": get_time_string(), + "RESPONSE_ID": self._response_id, + "RESPONSE_SIGNATURE": "", # initially unsigned } self._response_params.update(self._system_params) self._response_params.update(self._request_params) @@ -108,9 +108,11 @@ class Processor: def _decode_request(self): """Decodes _request_xml from _saml_request.""" - self._request_xml = utils.decode_base64_and_inflate(self._saml_request).decode('utf-8') + self._request_xml = utils.decode_base64_and_inflate(self._saml_request).decode( + "utf-8" + ) - self._logger.debug('SAML request decoded') + self._logger.debug("SAML request decoded") def _determine_assertion_id(self): """Determines the _assertion_id.""" @@ -119,7 +121,7 @@ class Processor: def _determine_audience(self): """Determines the _audience.""" self._audience = self._remote.audience - self._logger.info('determined audience') + self._logger.info("determined audience") def _determine_response_id(self): """Determines _response_id.""" @@ -138,85 +140,90 @@ class Processor: def _extract_saml_request(self): """Retrieves the _saml_request AuthnRequest from the _django_request.""" - self._saml_request = self._django_request.session['SAMLRequest'] - self._relay_state = self._django_request.session['RelayState'] + self._saml_request = self._django_request.session["SAMLRequest"] + self._relay_state = self._django_request.session["RelayState"] def _format_assertion(self): """Formats _assertion_params as _assertion_xml.""" # https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions - self._assertion_params['ATTRIBUTES'] = [ + self._assertion_params["ATTRIBUTES"] = [ { - 'FriendlyName': 'eduPersonPrincipalName', - 'Name': 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', - 'Value': self._django_request.user.email, + "FriendlyName": "eduPersonPrincipalName", + "Name": "urn:oid:1.3.6.1.4.1.5923.1.1.1.6", + "Value": self._django_request.user.email, }, { - 'FriendlyName': 'cn', - 'Name': 'urn:oid:2.5.4.3', - 'Value': self._django_request.user.name, + "FriendlyName": "cn", + "Name": "urn:oid:2.5.4.3", + "Value": self._django_request.user.name, }, { - 'FriendlyName': 'mail', - 'Name': 'urn:oid:0.9.2342.19200300.100.1.3', - 'Value': self._django_request.user.email, + "FriendlyName": "mail", + "Name": "urn:oid:0.9.2342.19200300.100.1.3", + "Value": self._django_request.user.email, }, { - 'FriendlyName': 'displayName', - 'Name': 'urn:oid:2.16.840.1.113730.3.1.241', - 'Value': self._django_request.user.username, + "FriendlyName": "displayName", + "Name": "urn:oid:2.16.840.1.113730.3.1.241", + "Value": self._django_request.user.username, }, { - 'FriendlyName': 'uid', - 'Name': 'urn:oid:0.9.2342.19200300.100.1.1', - 'Value': self._django_request.user.pk, + "FriendlyName": "uid", + "Name": "urn:oid:0.9.2342.19200300.100.1.1", + "Value": self._django_request.user.pk, }, ] from passbook.providers.saml.models import SAMLPropertyMapping + for mapping in self._remote.property_mappings.all().select_subclasses(): if isinstance(mapping, SAMLPropertyMapping): mapping_payload = { - 'Name': mapping.saml_name, - 'ValueArray': [], - 'FriendlyName': mapping.friendly_name + "Name": mapping.saml_name, + "ValueArray": [], + "FriendlyName": mapping.friendly_name, } for value in mapping.values: - mapping_payload['ValueArray'].append(value.format( - user=self._django_request.user, - request=self._django_request - )) - self._assertion_params['ATTRIBUTES'].append(mapping_payload) + mapping_payload["ValueArray"].append( + value.format( + user=self._django_request.user, request=self._django_request + ) + ) + self._assertion_params["ATTRIBUTES"].append(mapping_payload) self._assertion_xml = xml_render.get_assertion_xml( - 'saml/xml/assertions/generic.xml', self._assertion_params, signed=True) + "saml/xml/assertions/generic.xml", self._assertion_params, signed=True + ) def _format_response(self): """Formats _response_params as _response_xml.""" - assertion_id = self._assertion_params['ASSERTION_ID'] - self._response_xml = xml_render.get_response_xml(self._response_params, - saml_provider=self._remote, - assertion_id=assertion_id) + assertion_id = self._assertion_params["ASSERTION_ID"] + self._response_xml = xml_render.get_response_xml( + self._response_params, saml_provider=self._remote, assertion_id=assertion_id + ) def _get_django_response_params(self): """Returns a dictionary of parameters for the response template.""" return { - 'acs_url': self._request_params['ACS_URL'], - 'saml_response': self._saml_response, - 'relay_state': self._relay_state, - 'autosubmit': self._remote.application.skip_authorization, + "acs_url": self._request_params["ACS_URL"], + "saml_response": self._saml_response, + "relay_state": self._relay_state, + "autosubmit": self._remote.application.skip_authorization, } def _parse_request(self): """Parses various parameters from _request_xml into _request_params.""" # Minimal test to verify that it's not binarily encoded still: - if not str(self._request_xml.strip()).startswith('<'): - raise Exception('RequestXML is not valid XML; ' - 'it may need to be decoded or decompressed.') + if not str(self._request_xml.strip()).startswith("<"): + raise Exception( + "RequestXML is not valid XML; " + "it may need to be decoded or decompressed." + ) root = ElementTree.fromstring(self._request_xml) params = {} - params['ACS_URL'] = root.attrib['AssertionConsumerServiceURL'] - params['REQUEST_ID'] = root.attrib['ID'] - params['DESTINATION'] = root.attrib.get('Destination', '') - params['PROVIDER_NAME'] = root.attrib.get('ProviderName', '') + params["ACS_URL"] = root.attrib["AssertionConsumerServiceURL"] + params["REQUEST_ID"] = root.attrib["ID"] + params["DESTINATION"] = root.attrib.get("Destination", "") + params["PROVIDER_NAME"] = root.attrib.get("ProviderName", "") self._request_params = params def _reset(self, django_request, sp_config=None): @@ -240,10 +247,8 @@ class Processor: self._saml_response = sp_config self._session_index = sp_config self._subject = sp_config - self._subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' - self._system_params = { - 'ISSUER': self._remote.issuer - } + self._subject_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + self._system_params = {"ISSUER": self._remote.issuer} def _validate_request(self): """ @@ -255,11 +260,12 @@ class Processor: CannotHandleAssertion: if the ACS URL specified in the SAML request doesn't match the one specified in the processor config. """ - request_acs_url = self._request_params['ACS_URL'] + request_acs_url = self._request_params["ACS_URL"] if self._remote.acs_url != request_acs_url: - msg = ("couldn't find ACS url '{}' in SAML2IDP_REMOTES " - "setting.".format(request_acs_url)) + msg = "couldn't find ACS url '{}' in SAML2IDP_REMOTES " "setting.".format( + request_acs_url + ) self._logger.info(msg) raise exceptions.CannotHandleAssertion(msg) @@ -323,8 +329,8 @@ class Processor: # - Destination: Should be this IdP's SSO endpoint URL. Not used in the response? # - ProviderName: According to the spec, this is optional. self._request_params = { - 'ACS_URL': acs_url, - 'DESTINATION': '', - 'PROVIDER_NAME': '', + "ACS_URL": acs_url, + "DESTINATION": "", + "PROVIDER_NAME": "", } self._relay_state = url diff --git a/passbook/providers/saml/forms.py b/passbook/providers/saml/forms.py index dc2ea0f72..df059cd3f 100644 --- a/passbook/providers/saml/forms.py +++ b/passbook/providers/saml/forms.py @@ -5,37 +5,52 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ from passbook.lib.fields import DynamicArrayField -from passbook.providers.saml.models import (SAMLPropertyMapping, SAMLProvider, - get_provider_choices) +from passbook.providers.saml.models import ( + SAMLPropertyMapping, + SAMLProvider, + get_provider_choices, +) from passbook.providers.saml.utils import CertificateBuilder class SAMLProviderForm(forms.ModelForm): """SAML Provider form""" - processor_path = forms.ChoiceField(choices=get_provider_choices(), label='Processor') + processor_path = forms.ChoiceField( + choices=get_provider_choices(), label="Processor" + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) builder = CertificateBuilder() builder.build() - self.fields['signing_cert'].initial = builder.certificate - self.fields['signing_key'].initial = builder.private_key + self.fields["signing_cert"].initial = builder.certificate + self.fields["signing_key"].initial = builder.private_key class Meta: model = SAMLProvider - fields = ['name', 'property_mappings', 'acs_url', 'audience', 'processor_path', 'issuer', - 'assertion_valid_for', 'signing', 'signing_cert', 'signing_key', ] + fields = [ + "name", + "property_mappings", + "acs_url", + "audience", + "processor_path", + "issuer", + "assertion_valid_for", + "signing", + "signing_cert", + "signing_key", + ] labels = { - 'acs_url': 'ACS URL', - 'signing_cert': 'Singing Certificate', + "acs_url": "ACS URL", + "signing_cert": "Singing Certificate", } widgets = { - 'name': forms.TextInput(), - 'audience': forms.TextInput(), - 'issuer': forms.TextInput(), - 'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False) + "name": forms.TextInput(), + "audience": forms.TextInput(), + "issuer": forms.TextInput(), + "property_mappings": FilteredSelectMultiple(_("Property Mappings"), False), } @@ -45,15 +60,13 @@ class SAMLPropertyMappingForm(forms.ModelForm): class Meta: model = SAMLPropertyMapping - fields = ['name', 'saml_name', 'friendly_name', 'values'] + fields = ["name", "saml_name", "friendly_name", "values"] widgets = { - 'name': forms.TextInput(), - 'saml_name': forms.TextInput(), - 'friendly_name': forms.TextInput(), - } - field_classes = { - 'values': DynamicArrayField + "name": forms.TextInput(), + "saml_name": forms.TextInput(), + "friendly_name": forms.TextInput(), } + field_classes = {"values": DynamicArrayField} help_texts = { - 'values': 'String substitution uses a syntax like "{variable} test}".' + "values": 'String substitution uses a syntax like "{variable} test}".' } diff --git a/passbook/providers/saml/migrations/0001_initial.py b/passbook/providers/saml/migrations/0001_initial.py index 8839bb5a9..6667a5e4e 100644 --- a/passbook/providers/saml/migrations/0001_initial.py +++ b/passbook/providers/saml/migrations/0001_initial.py @@ -10,42 +10,70 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='SAMLPropertyMapping', + name="SAMLPropertyMapping", fields=[ - ('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')), - ('saml_name', models.TextField()), - ('friendly_name', models.TextField(blank=True, default=None, null=True)), - ('values', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)), + ( + "propertymapping_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PropertyMapping", + ), + ), + ("saml_name", models.TextField()), + ( + "friendly_name", + models.TextField(blank=True, default=None, null=True), + ), + ( + "values", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), ], options={ - 'verbose_name': 'SAML Property Mapping', - 'verbose_name_plural': 'SAML Property Mappings', + "verbose_name": "SAML Property Mapping", + "verbose_name_plural": "SAML Property Mappings", }, - bases=('passbook_core.propertymapping',), + bases=("passbook_core.propertymapping",), ), migrations.CreateModel( - name='SAMLProvider', + name="SAMLProvider", fields=[ - ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), - ('name', models.TextField()), - ('acs_url', models.URLField()), - ('audience', models.TextField(default='')), - ('processor_path', models.CharField(max_length=255)), - ('issuer', models.TextField()), - ('assertion_valid_for', models.IntegerField(default=86400)), - ('signing', models.BooleanField(default=True)), - ('signing_cert', models.TextField()), - ('signing_key', models.TextField()), + ( + "provider_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Provider", + ), + ), + ("name", models.TextField()), + ("acs_url", models.URLField()), + ("audience", models.TextField(default="")), + ("processor_path", models.CharField(max_length=255)), + ("issuer", models.TextField()), + ("assertion_valid_for", models.IntegerField(default=86400)), + ("signing", models.BooleanField(default=True)), + ("signing_cert", models.TextField()), + ("signing_key", models.TextField()), ], options={ - 'verbose_name': 'SAML Provider', - 'verbose_name_plural': 'SAML Providers', + "verbose_name": "SAML Provider", + "verbose_name_plural": "SAML Providers", }, - bases=('passbook_core.provider',), + bases=("passbook_core.provider",), ), ] diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 3c1ec2f5e..3087cabad 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -17,7 +17,7 @@ class SAMLProvider(Provider): name = models.TextField() acs_url = models.URLField() - audience = models.TextField(default='') + audience = models.TextField(default="") processor_path = models.CharField(max_length=255, choices=[]) issuer = models.TextField() assertion_valid_for = models.IntegerField(default=86400) @@ -25,12 +25,12 @@ class SAMLProvider(Provider): signing_cert = models.TextField() signing_key = models.TextField() - form = 'passbook.providers.saml.forms.SAMLProviderForm' + form = "passbook.providers.saml.forms.SAMLProviderForm" _processor = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._meta.get_field('processor_path').choices = get_provider_choices() + self._meta.get_field("processor_path").choices = get_provider_choices() @property def processor(self): @@ -50,15 +50,17 @@ class SAMLProvider(Provider): """Get link to download XML metadata for admin interface""" try: # pylint: disable=no-member - return reverse('passbook_providers_saml:saml-metadata', - kwargs={'application': self.application.slug}) + return reverse( + "passbook_providers_saml:saml-metadata", + kwargs={"application": self.application.slug}, + ) except Provider.application.RelatedObjectDoesNotExist: return None class Meta: - verbose_name = _('SAML Provider') - verbose_name_plural = _('SAML Providers') + verbose_name = _("SAML Provider") + verbose_name_plural = _("SAML Providers") class SAMLPropertyMapping(PropertyMapping): @@ -68,15 +70,16 @@ class SAMLPropertyMapping(PropertyMapping): friendly_name = models.TextField(default=None, blank=True, null=True) values = ArrayField(models.TextField()) - form = 'passbook.providers.saml.forms.SAMLPropertyMappingForm' + form = "passbook.providers.saml.forms.SAMLPropertyMappingForm" def __str__(self): return "SAML Property Mapping %s" % self.saml_name class Meta: - verbose_name = _('SAML Property Mapping') - verbose_name_plural = _('SAML Property Mappings') + verbose_name = _("SAML Property Mapping") + verbose_name_plural = _("SAML Property Mappings") + def get_provider_choices(): """Return tuple of class_path, class name of all providers.""" diff --git a/passbook/providers/saml/processors/salesforce.py b/passbook/providers/saml/processors/salesforce.py index 178773372..871836e29 100644 --- a/passbook/providers/saml/processors/salesforce.py +++ b/passbook/providers/saml/processors/salesforce.py @@ -8,8 +8,9 @@ class SalesForceProcessor(Processor): """SalesForce.com-specific SAML 2.0 AuthnRequest to Response Handler Processor.""" def _determine_audience(self): - self._audience = 'IAMShowcase' + self._audience = "IAMShowcase" def _format_assertion(self): self._assertion_xml = get_assertion_xml( - 'saml/xml/assertions/salesforce.xml', self._assertion_params, signed=True) + "saml/xml/assertions/salesforce.xml", self._assertion_params, signed=True + ) diff --git a/passbook/providers/saml/settings.py b/passbook/providers/saml/settings.py index 0d639b84c..6821f5172 100644 --- a/passbook/providers/saml/settings.py +++ b/passbook/providers/saml/settings.py @@ -1,6 +1,6 @@ """saml provider settings""" PASSBOOK_PROVIDERS_SAML_PROCESSORS = [ - 'passbook.providers.saml.processors.generic', - 'passbook.providers.saml.processors.salesforce', + "passbook.providers.saml.processors.generic", + "passbook.providers.saml.processors.salesforce", ] diff --git a/passbook/providers/saml/urls.py b/passbook/providers/saml/urls.py index 4e7d8fdd2..a8777ba79 100644 --- a/passbook/providers/saml/urls.py +++ b/passbook/providers/saml/urls.py @@ -4,14 +4,28 @@ from django.urls import path from passbook.providers.saml import views urlpatterns = [ - path('/login/', - views.LoginBeginView.as_view(), name="saml-login"), - path('/login/initiate/', - views.InitiateLoginView.as_view(), name="saml-login-initiate"), - path('/login/process/', - views.LoginProcessView.as_view(), name='saml-login-process'), - path('/logout/', views.LogoutView.as_view(), name="saml-logout"), - path('/logout/slo/', views.SLOLogout.as_view(), name="saml-logout-slo"), - path('/metadata/', - views.DescriptorDownloadView.as_view(), name='saml-metadata'), + path( + "/login/", views.LoginBeginView.as_view(), name="saml-login" + ), + path( + "/login/initiate/", + views.InitiateLoginView.as_view(), + name="saml-login-initiate", + ), + path( + "/login/process/", + views.LoginProcessView.as_view(), + name="saml-login-process", + ), + path("/logout/", views.LogoutView.as_view(), name="saml-logout"), + path( + "/logout/slo/", + views.SLOLogout.as_view(), + name="saml-logout-slo", + ), + path( + "/metadata/", + views.DescriptorDownloadView.as_view(), + name="saml-metadata", + ), ] diff --git a/passbook/providers/saml/utils.py b/passbook/providers/saml/utils.py index a3c8527f7..1b030102b 100644 --- a/passbook/providers/saml/utils.py +++ b/passbook/providers/saml/utils.py @@ -26,7 +26,7 @@ def deflate_and_base64_encode(string_val): def nice64(src): """ Returns src base64-encoded and formatted nicely for our XML. """ - return base64.b64encode(src).decode('utf-8').replace('\n', '') + return base64.b64encode(src).decode("utf-8").replace("\n", "") class CertificateBuilder: @@ -47,28 +47,44 @@ class CertificateBuilder: """Build self-signed certificate""" one_day = datetime.timedelta(1, 0, 0) self.__private_key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend() + public_exponent=65537, key_size=2048, backend=default_backend() ) self.__public_key = self.__private_key.public_key() - self.__builder = \ - x509.CertificateBuilder(). \ - subject_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, u'passbook Self-signed SAML Certificate'), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'passbook'), - x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Self-signed'), - ])). \ - issuer_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, u'passbook Self-signed SAML Certificate'), - ])). \ - not_valid_before(datetime.datetime.today() - one_day). \ - not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365)). \ - serial_number(int(uuid.uuid4())). \ - public_key(self.__public_key) + self.__builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, + u"passbook Self-signed SAML Certificate", + ), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"passbook"), + x509.NameAttribute( + NameOID.ORGANIZATIONAL_UNIT_NAME, u"Self-signed" + ), + ] + ) + ) + .issuer_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, + u"passbook Self-signed SAML Certificate", + ), + ] + ) + ) + .not_valid_before(datetime.datetime.today() - one_day) + .not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365)) + .serial_number(int(uuid.uuid4())) + .public_key(self.__public_key) + ) self.__certificate = self.__builder.sign( - private_key=self.__private_key, algorithm=hashes.SHA256(), - backend=default_backend() + private_key=self.__private_key, + algorithm=hashes.SHA256(), + backend=default_backend(), ) @property @@ -78,11 +94,11 @@ class CertificateBuilder: encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), - ).decode('utf-8') + ).decode("utf-8") @property def certificate(self): """Return certificate in PEM format""" return self.__certificate.public_bytes( encoding=serialization.Encoding.PEM, - ).decode('utf-8') + ).decode("utf-8") diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py index a932cd12c..f78e363a1 100644 --- a/passbook/providers/saml/views.py +++ b/passbook/providers/saml/views.py @@ -22,21 +22,21 @@ from passbook.providers.saml import exceptions from passbook.providers.saml.models import SAMLProvider LOGGER = get_logger() -URL_VALIDATOR = URLValidator(schemes=('http', 'https')) +URL_VALIDATOR = URLValidator(schemes=("http", "https")) def _generate_response(request, provider: SAMLProvider): """Generate a SAML response using processor_instance and return it in the proper Django response.""" try: - provider.processor.init_deep_link(request, '') + provider.processor.init_deep_link(request, "") ctx = provider.processor.generate_response() - ctx['remote'] = provider - ctx['is_login'] = True + ctx["remote"] = provider + ctx["is_login"] = True except exceptions.UserNotAuthorized: - return render(request, 'saml/idp/invalid_user.html') + return render(request, "saml/idp/invalid_user.html") - return render(request, 'saml/idp/login.html', ctx) + return render(request, "saml/idp/login.html", ctx) def render_xml(request, template, ctx): @@ -53,14 +53,17 @@ class AccessRequiredView(AccessMixin, View): def provider(self): """Get provider instance""" if not self._provider: - application = get_object_or_404(Application, slug=self.kwargs['application']) + application = get_object_or_404( + Application, slug=self.kwargs["application"] + ) self._provider = get_object_or_404(SAMLProvider, pk=application.provider_id) return self._provider def _has_access(self): """Check if user has access to application""" - policy_engine = PolicyEngine(self.provider.application.policies.all(), - self.request.user, self.request) + policy_engine = PolicyEngine( + self.provider.application.policies.all(), self.request.user, self.request + ) policy_engine.build() return policy_engine.passing @@ -68,10 +71,14 @@ class AccessRequiredView(AccessMixin, View): if not request.user.is_authenticated: return self.handle_no_permission() if not self._has_access(): - return render(request, 'login/denied.html', { - 'title': _("You don't have access to this application"), - 'is_login': True - }) + return render( + request, + "login/denied.html", + { + "title": _("You don't have access to this application"), + "is_login": True, + }, + ) return super().dispatch(request, *args, **kwargs) @@ -81,21 +88,24 @@ class LoginBeginView(AccessRequiredView): @method_decorator(csrf_exempt) def dispatch(self, request, application): - if request.method == 'POST': + if request.method == "POST": source = request.POST else: source = request.GET # Store these values now, because Django's login cycle won't preserve them. try: - request.session['SAMLRequest'] = source['SAMLRequest'] + request.session["SAMLRequest"] = source["SAMLRequest"] except (KeyError, MultiValueDictKeyError): - return HttpResponseBadRequest('the SAML request payload is missing') + return HttpResponseBadRequest("the SAML request payload is missing") - request.session['RelayState'] = source.get('RelayState', '') - return redirect(reverse('passbook_providers_saml:saml-login-process', kwargs={ - 'application': application - })) + request.session["RelayState"] = source.get("RelayState", "") + return redirect( + reverse( + "passbook_providers_saml:saml-login-process", + kwargs={"application": application}, + ) + ) class RedirectToSPView(AccessRequiredView): @@ -103,13 +113,14 @@ class RedirectToSPView(AccessRequiredView): def get(self, request, acs_url, saml_response, relay_state): """Return autosubmit form""" - return render(request, 'core/autosubmit_form.html', { - 'url': acs_url, - 'attrs': { - 'SAMLResponse': saml_response, - 'RelayState': relay_state - } - }) + return render( + request, + "core/autosubmit_form.html", + { + "url": acs_url, + "attrs": {"SAMLResponse": saml_response, "RelayState": relay_state}, + }, + ) class LoginProcessView(AccessRequiredView): @@ -119,19 +130,22 @@ class LoginProcessView(AccessRequiredView): # pylint: disable=unused-argument def get(self, request, application): """Handle get request, i.e. render form""" - LOGGER.debug("SAMLLoginProcessView", request=request, method='get') + LOGGER.debug("SAMLLoginProcessView", request=request, method="get") # Check if user has access if self.provider.application.skip_authorization: ctx = self.provider.processor.generate_response() # Log Application Authorization - Event.new(EventAction.AUTHORIZE_APPLICATION, - authorized_application=self.provider.application.pk, - skipped_authorization=True).from_http(request) + Event.new( + EventAction.AUTHORIZE_APPLICATION, + authorized_application=self.provider.application.pk, + skipped_authorization=True, + ).from_http(request) return RedirectToSPView.as_view()( request=request, - acs_url=ctx['acs_url'], - saml_response=ctx['saml_response'], - relay_state=ctx['relay_state']) + acs_url=ctx["acs_url"], + saml_response=ctx["saml_response"], + relay_state=ctx["relay_state"], + ) try: full_res = _generate_response(request, self.provider) return full_res @@ -141,18 +155,21 @@ class LoginProcessView(AccessRequiredView): # pylint: disable=unused-argument def post(self, request, application): """Handle post request, return back to ACS""" - LOGGER.debug("SAMLLoginProcessView", request=request, method='post') + LOGGER.debug("SAMLLoginProcessView", request=request, method="post") # Check if user has access - if request.POST.get('ACSUrl', None): + if request.POST.get("ACSUrl", None): # User accepted request - Event.new(EventAction.AUTHORIZE_APPLICATION, - authorized_application=self.provider.application.pk, - skipped_authorization=False).from_http(request) + Event.new( + EventAction.AUTHORIZE_APPLICATION, + authorized_application=self.provider.application.pk, + skipped_authorization=False, + ).from_http(request) return RedirectToSPView.as_view()( request=request, - acs_url=request.POST.get('ACSUrl'), - saml_response=request.POST.get('SAMLResponse'), - relay_state=request.POST.get('RelayState')) + acs_url=request.POST.get("ACSUrl"), + saml_response=request.POST.get("SAMLResponse"), + relay_state=request.POST.get("RelayState"), + ) try: full_res = _generate_response(request, self.provider) return full_res @@ -170,7 +187,7 @@ class LogoutView(CSRFExemptMixin, AccessRequiredView): """Perform logout""" logout(request) - redirect_url = request.GET.get('redirect_to', '') + redirect_url = request.GET.get("redirect_to", "") try: URL_VALIDATOR(redirect_url) @@ -179,7 +196,7 @@ class LogoutView(CSRFExemptMixin, AccessRequiredView): else: return redirect(redirect_url) - return render(request, 'saml/idp/logged_out.html') + return render(request, "saml/idp/logged_out.html") class SLOLogout(CSRFExemptMixin, AccessRequiredView): @@ -189,14 +206,14 @@ class SLOLogout(CSRFExemptMixin, AccessRequiredView): # pylint: disable=unused-argument def post(self, request, application): """Perform logout""" - request.session['SAMLRequest'] = request.POST['SAMLRequest'] + request.session["SAMLRequest"] = request.POST["SAMLRequest"] # TODO: Parse SAML LogoutRequest from POST data, similar to login_process(). # TODO: Modify the base processor to handle logouts? # TODO: Combine this with login_process(), since they are so very similar? # TODO: Format a LogoutResponse and return it to the browser. # XXX: For now, simply log out without validating the request. logout(request) - return render(request, 'saml/idp/logged_out.html') + return render(request, "saml/idp/logged_out.html") class DescriptorDownloadView(AccessRequiredView): @@ -205,23 +222,32 @@ class DescriptorDownloadView(AccessRequiredView): def get(self, request, application): """Replies with the XML Metadata IDSSODescriptor.""" entity_id = self.provider.issuer - slo_url = request.build_absolute_uri(reverse('passbook_providers_saml:saml-logout', kwargs={ - 'application': application - })) - sso_url = request.build_absolute_uri(reverse('passbook_providers_saml:saml-login', kwargs={ - 'application': application - })) - pubkey = strip_pem_header(self.provider.signing_cert.replace('\r', '')).replace('\n', '') + slo_url = request.build_absolute_uri( + reverse( + "passbook_providers_saml:saml-logout", + kwargs={"application": application}, + ) + ) + sso_url = request.build_absolute_uri( + reverse( + "passbook_providers_saml:saml-login", + kwargs={"application": application}, + ) + ) + pubkey = strip_pem_header(self.provider.signing_cert.replace("\r", "")).replace( + "\n", "" + ) ctx = { - 'entity_id': entity_id, - 'cert_public_key': pubkey, - 'slo_url': slo_url, - 'sso_url': sso_url + "entity_id": entity_id, + "cert_public_key": pubkey, + "slo_url": slo_url, + "sso_url": sso_url, } - metadata = render_to_string('saml/xml/metadata.xml', ctx) - response = HttpResponse(metadata, content_type='application/xml') - response['Content-Disposition'] = ('attachment; filename="' - '%s_passbook_meta.xml"' % self.provider.name) + metadata = render_to_string("saml/xml/metadata.xml", ctx) + response = HttpResponse(metadata, content_type="application/xml") + response["Content-Disposition"] = ( + 'attachment; filename="' '%s_passbook_meta.xml"' % self.provider.name + ) return response @@ -231,6 +257,6 @@ class InitiateLoginView(AccessRequiredView): # pylint: disable=unused-argument def get(self, request, application): """Initiates an IdP-initiated link to a simple SP resource/target URL.""" - self.provider.processor.init_deep_link(request, '') + self.provider.processor.init_deep_link(request, "") self.provider.processor.is_idp_initiated = True return _generate_response(request, self.provider) diff --git a/passbook/providers/saml/xml_render.py b/passbook/providers/saml/xml_render.py index 986bc5cbd..8ac7c0577 100644 --- a/passbook/providers/saml/xml_render.py +++ b/passbook/providers/saml/xml_render.py @@ -6,8 +6,7 @@ from typing import TYPE_CHECKING from structlog import get_logger from passbook.lib.utils.template import render_to_string -from passbook.providers.saml.xml_signing import (get_signature_xml, - sign_with_signxml) +from passbook.providers.saml.xml_signing import get_signature_xml, sign_with_signxml if TYPE_CHECKING: from passbook.providers.saml.models import SAMLProvider @@ -20,13 +19,14 @@ def _get_attribute_statement(params): Modifies the params dict. PRE-REQ: params['SUBJECT'] has already been created (usually by a call to _get_subject().""" - attributes = params.get('ATTRIBUTES', []) + attributes = params.get("ATTRIBUTES", []) if not attributes: - params['ATTRIBUTE_STATEMENT'] = '' + params["ATTRIBUTE_STATEMENT"] = "" return # Build complete AttributeStatement. - params['ATTRIBUTE_STATEMENT'] = render_to_string('saml/xml/attributes.xml', { - 'attributes': attributes}) + params["ATTRIBUTE_STATEMENT"] = render_to_string( + "saml/xml/attributes.xml", {"attributes": attributes} + ) def _get_in_response_to(params): @@ -36,16 +36,16 @@ def _get_in_response_to(params): # current design requires this; maybe refactor using better templates, or # just bite the bullet and use elementtree to produce the XML; see comments # in xml_templates about Canonical XML. - request_id = params.get('REQUEST_ID', None) + request_id = params.get("REQUEST_ID", None) if request_id: - params['IN_RESPONSE_TO'] = 'InResponseTo="%s" ' % request_id + params["IN_RESPONSE_TO"] = 'InResponseTo="%s" ' % request_id else: - params['IN_RESPONSE_TO'] = '' + params["IN_RESPONSE_TO"] = "" def _get_subject(params): """Insert Subject. Modifies the params dict.""" - params['SUBJECT_STATEMENT'] = render_to_string('saml/xml/subject.xml', params) + params["SUBJECT_STATEMENT"] = render_to_string("saml/xml/subject.xml", params) def get_assertion_xml(template, parameters, signed=False): @@ -53,7 +53,7 @@ def get_assertion_xml(template, parameters, signed=False): # Reset signature. params = {} params.update(parameters) - params['ASSERTION_SIGNATURE'] = '' + params["ASSERTION_SIGNATURE"] = "" _get_in_response_to(params) _get_subject(params) # must come before _get_attribute_statement() @@ -66,29 +66,32 @@ def get_assertion_xml(template, parameters, signed=False): # Sign it. signature_xml = get_signature_xml() - params['ASSERTION_SIGNATURE'] = signature_xml + params["ASSERTION_SIGNATURE"] = signature_xml return render_to_string(template, params) -def get_response_xml(parameters, saml_provider: SAMLProvider, assertion_id=''): +def get_response_xml(parameters, saml_provider: SAMLProvider, assertion_id=""): """Returns XML for response, with signatures, if signed is True.""" # Reset signatures. params = {} params.update(parameters) - params['RESPONSE_SIGNATURE'] = '' + params["RESPONSE_SIGNATURE"] = "" _get_in_response_to(params) - raw_response = render_to_string('saml/xml/response.xml', params) + raw_response = render_to_string("saml/xml/response.xml", params) # LOGGER.debug('Unsigned: %s', unsigned) if not saml_provider.signing: return raw_response signature_xml = get_signature_xml() - params['RESPONSE_SIGNATURE'] = signature_xml + params["RESPONSE_SIGNATURE"] = signature_xml # LOGGER.debug("Raw response: %s", raw_response) signed = sign_with_signxml( - saml_provider.signing_key, raw_response, saml_provider.signing_cert, - reference_uri=assertion_id) + saml_provider.signing_key, + raw_response, + saml_provider.signing_cert, + reference_uri=assertion_id, + ) return signed diff --git a/passbook/providers/saml/xml_signing.py b/passbook/providers/saml/xml_signing.py index 97bf2013e..cd5669f2b 100644 --- a/passbook/providers/saml/xml_signing.py +++ b/passbook/providers/saml/xml_signing.py @@ -13,17 +13,19 @@ LOGGER = get_logger() def sign_with_signxml(private_key, data, cert, reference_uri=None): """Sign Data with signxml""" key = serialization.load_pem_private_key( - str.encode('\n'.join([x.strip() for x in private_key.split('\n')])), - password=None, backend=default_backend()) + str.encode("\n".join([x.strip() for x in private_key.split("\n")])), + password=None, + backend=default_backend(), + ) # defused XML is not used here because it messes up XML namespaces # Data is trusted, so lxml is ok - root = etree.fromstring(data) # nosec - signer = XMLSigner(c14n_algorithm='http://www.w3.org/2001/10/xml-exc-c14n#') + root = etree.fromstring(data) # nosec + signer = XMLSigner(c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#") signed = signer.sign(root, key=key, cert=[cert], reference_uri=reference_uri) XMLVerifier().verify(signed, x509_cert=cert) - return etree.tostring(signed).decode('utf-8') # nosec + return etree.tostring(signed).decode("utf-8") # nosec def get_signature_xml(): """Returns XML Signature for subject.""" - return render_to_string('saml/xml/signature.xml', {}) + return render_to_string("saml/xml/signature.xml", {}) diff --git a/passbook/recovery/apps.py b/passbook/recovery/apps.py index 0a6db1a06..def3c603d 100644 --- a/passbook/recovery/apps.py +++ b/passbook/recovery/apps.py @@ -5,7 +5,7 @@ from django.apps import AppConfig class PassbookRecoveryConfig(AppConfig): """passbook Recovery app config""" - name = 'passbook.recovery' - label = 'passbook_recovery' - verbose_name = 'passbook Recovery' - mountpoint = 'recovery/' + name = "passbook.recovery" + label = "passbook_recovery" + verbose_name = "passbook Recovery" + mountpoint = "recovery/" diff --git a/passbook/recovery/management/commands/create_recovery_key.py b/passbook/recovery/management/commands/create_recovery_key.py index 49a3fc3ef..060bd55f4 100644 --- a/passbook/recovery/management/commands/create_recovery_key.py +++ b/passbook/recovery/management/commands/create_recovery_key.py @@ -17,30 +17,40 @@ LOGGER = get_logger() class Command(BaseCommand): """Create Nonce used to recover access""" - help = _('Create a Key which can be used to restore access to passbook.') + help = _("Create a Key which can be used to restore access to passbook.") def add_arguments(self, parser): - parser.add_argument('duration', default=1, action='store', - help='How long the token is valid for (in years).') - parser.add_argument('user', action='store', - help='Which user the Token gives access to.') + parser.add_argument( + "duration", + default=1, + action="store", + help="How long the token is valid for (in years).", + ) + parser.add_argument( + "user", action="store", help="Which user the Token gives access to." + ) def get_url(self, nonce: Nonce) -> str: """Get full recovery link""" - path = reverse('passbook_recovery:use-nonce', kwargs={'uuid': str(nonce.uuid)}) + path = reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)}) return f"https://{CONFIG.y('domain')}{path}" def handle(self, *args, **options): """Create Nonce used to recover access""" - duration = int(options.get('duration', 1)) + duration = int(options.get("duration", 1)) delta = timedelta(days=duration * 365.2425) _now = now() expiry = _now + delta - user = User.objects.get(username=options.get('user')) + user = User.objects.get(username=options.get("user")) nonce = Nonce.objects.create( expires=expiry, user=user, - description=f'Recovery Nonce generated by {getuser()} on {_now}') - self.stdout.write((f"Store this link safely, as it will allow" - f" anyone to access passbook as {user}.")) + description=f"Recovery Nonce generated by {getuser()} on {_now}", + ) + self.stdout.write( + ( + f"Store this link safely, as it will allow" + f" anyone to access passbook as {user}." + ) + ) self.stdout.write(self.get_url(nonce)) diff --git a/passbook/recovery/tests.py b/passbook/recovery/tests.py index d6650fa2e..bb2c19b68 100644 --- a/passbook/recovery/tests.py +++ b/passbook/recovery/tests.py @@ -12,21 +12,22 @@ class TestRecovery(TestCase): """recovery tests""" def setUp(self): - self.user = User.objects.create_user( - username='recovery-test-user') + self.user = User.objects.create_user(username="recovery-test-user") def test_create_key(self): """Test creation of a new key""" out = StringIO() self.assertEqual(len(Nonce.objects.all()), 0) - call_command('create_recovery_key', '1', self.user.username, stdout=out) - self.assertIn('https://localhost/recovery/use-nonce/', out.getvalue()) + call_command("create_recovery_key", "1", self.user.username, stdout=out) + self.assertIn("https://localhost/recovery/use-nonce/", out.getvalue()) self.assertEqual(len(Nonce.objects.all()), 1) def test_recovery_view(self): """Test recovery view""" out = StringIO() - call_command('create_recovery_key', '1', self.user.username, stdout=out) + call_command("create_recovery_key", "1", self.user.username, stdout=out) nonce = Nonce.objects.first() - self.client.get(reverse('passbook_recovery:use-nonce', kwargs={'uuid': str(nonce.uuid)})) - self.assertEqual(int(self.client.session['_auth_user_id']), nonce.user.pk) + self.client.get( + reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)}) + ) + self.assertEqual(int(self.client.session["_auth_user_id"]), nonce.user.pk) diff --git a/passbook/recovery/urls.py b/passbook/recovery/urls.py index 36a3f93f6..1be39f0ff 100644 --- a/passbook/recovery/urls.py +++ b/passbook/recovery/urls.py @@ -5,5 +5,5 @@ from django.urls import path from passbook.recovery.views import UseNonceView urlpatterns = [ - path('use-nonce//', UseNonceView.as_view(), name='use-nonce'), + path("use-nonce//", UseNonceView.as_view(), name="use-nonce"), ] diff --git a/passbook/recovery/views.py b/passbook/recovery/views.py index fd870138e..d16760e52 100644 --- a/passbook/recovery/views.py +++ b/passbook/recovery/views.py @@ -18,7 +18,7 @@ class UseNonceView(View): if nonce.is_expired: nonce.delete() raise Http404 - login(request, nonce.user, backend='django.contrib.auth.backends.ModelBackend') + login(request, nonce.user, backend="django.contrib.auth.backends.ModelBackend") nonce.delete() messages.warning(request, _("Used recovery-link to authenticate.")) - return redirect('passbook_core:overview') + return redirect("passbook_core:overview") diff --git a/passbook/root/celery.py b/passbook/root/celery.py index 2b7c61286..c8d12aab6 100644 --- a/passbook/root/celery.py +++ b/passbook/root/celery.py @@ -3,8 +3,7 @@ import os from logging.config import dictConfig from celery import Celery -from celery.signals import (after_task_publish, setup_logging, task_postrun, - task_prerun) +from celery.signals import after_task_publish, setup_logging, task_postrun, task_prerun from django.conf import settings from structlog import get_logger @@ -12,7 +11,7 @@ from structlog import get_logger os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings") LOGGER = get_logger() -CELERY_APP = Celery('passbook') +CELERY_APP = Celery("passbook") # pylint: disable=unused-argument @@ -26,32 +25,31 @@ def config_loggers(*args, **kwags): @after_task_publish.connect def after_task_publish(sender=None, headers=None, body=None, **kwargs): """Log task_id after it was published""" - info = headers if 'task' in headers else body - LOGGER.debug('Task published', task_id=info.get('id', ''), task_name=info.get('task', '')) + info = headers if "task" in headers else body + LOGGER.debug( + "Task published", task_id=info.get("id", ""), task_name=info.get("task", "") + ) # pylint: disable=unused-argument @task_prerun.connect def task_prerun(task_id, task, *args, **kwargs): """Log task_id on worker""" - LOGGER.debug('Task started', task_id=task_id, task_name=task.__name__) + LOGGER.debug("Task started", task_id=task_id, task_name=task.__name__) # pylint: disable=unused-argument @task_postrun.connect def task_postrun(task_id, task, *args, retval=None, state=None, **kwargs): """Log task_id on worker""" - LOGGER.debug('Task finished', - task_id=task_id, - task_name=task.__name__, - state=state) + LOGGER.debug("Task finished", task_id=task_id, task_name=task.__name__, state=state) # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -CELERY_APP.config_from_object(settings, namespace='CELERY') +CELERY_APP.config_from_object(settings, namespace="CELERY") # Load task modules from all registered Django app configs. CELERY_APP.autodiscover_tasks() diff --git a/passbook/root/monitoring.py b/passbook/root/monitoring.py index c7c824715..33ffc0fca 100644 --- a/passbook/root/monitoring.py +++ b/passbook/root/monitoring.py @@ -12,12 +12,12 @@ class MetricsView(View): def get(self, request: HttpRequest) -> HttpResponse: """Check for HTTP-Basic auth""" - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - token_type, _, credentials = auth_header.partition(' ') + auth_header = request.META.get("HTTP_AUTHORIZATION", "") + token_type, _, credentials = auth_header.partition(" ") creds = f"monitor:{settings.SECRET_KEY}" expected = b64encode(str.encode(creds)).decode() - if token_type != 'Basic' or credentials != expected: + if token_type != "Basic" or credentials != expected: raise Http404 return ExportToDjangoView(request) diff --git a/passbook/root/settings.py b/passbook/root/settings.py index b557f1c73..39b066c19 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -28,102 +28,98 @@ LOGGER = structlog.get_logger() # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -STATIC_ROOT = BASE_DIR + '/static' +STATIC_ROOT = BASE_DIR + "/static" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = CONFIG.y('secret_key', - "9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s") # noqa Debug +SECRET_KEY = CONFIG.y( + "secret_key", "9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s" +) # noqa Debug -DEBUG = CONFIG.y_bool('debug') -INTERNAL_IPS = ['127.0.0.1'] -ALLOWED_HOSTS = ['*'] -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +DEBUG = CONFIG.y_bool("debug") +INTERNAL_IPS = ["127.0.0.1"] +ALLOWED_HOSTS = ["*"] +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -LOGIN_URL = 'passbook_core:auth-login' +LOGIN_URL = "passbook_core:auth-login" # CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view' # Custom user model -AUTH_USER_MODEL = 'passbook_core.User' +AUTH_USER_MODEL = "passbook_core.User" -CSRF_COOKIE_NAME = 'passbook_csrf' -SESSION_COOKIE_NAME = 'passbook_session' -SESSION_COOKIE_DOMAIN = CONFIG.y('domain', None) -LANGUAGE_COOKIE_NAME = 'passbook_language' +CSRF_COOKIE_NAME = "passbook_csrf" +SESSION_COOKIE_NAME = "passbook_session" +SESSION_COOKIE_DOMAIN = CONFIG.y("domain", None) +LANGUAGE_COOKIE_NAME = "passbook_language" AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - 'guardian.backends.ObjectPermissionBackend', + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", ] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.postgres', - 'rest_framework', - 'drf_yasg', - 'guardian', - 'django_prometheus', - - 'passbook.core.apps.PassbookCoreConfig', - 'passbook.admin.apps.PassbookAdminConfig', - 'passbook.api.apps.PassbookAPIConfig', - 'passbook.lib.apps.PassbookLibConfig', - 'passbook.audit.apps.PassbookAuditConfig', - 'passbook.recovery.apps.PassbookRecoveryConfig', - - 'passbook.sources.saml.apps.PassbookSourceSAMLConfig', - 'passbook.sources.ldap.apps.PassbookSourceLDAPConfig', - 'passbook.sources.oauth.apps.PassbookSourceOAuthConfig', - - 'passbook.providers.app_gw.apps.PassbookApplicationApplicationGatewayConfig', - 'passbook.providers.oauth.apps.PassbookProviderOAuthConfig', - 'passbook.providers.oidc.apps.PassbookProviderOIDCConfig', - 'passbook.providers.saml.apps.PassbookProviderSAMLConfig', - - 'passbook.factors.otp.apps.PassbookFactorOTPConfig', - 'passbook.factors.captcha.apps.PassbookFactorCaptchaConfig', - 'passbook.factors.password.apps.PassbookFactorPasswordConfig', - 'passbook.factors.dummy.apps.PassbookFactorDummyConfig', - 'passbook.factors.email.apps.PassbookFactorEmailConfig', - - 'passbook.policies.expiry.apps.PassbookPolicyExpiryConfig', - 'passbook.policies.reputation.apps.PassbookPolicyReputationConfig', - 'passbook.policies.hibp.apps.PassbookPolicyHIBPConfig', - 'passbook.policies.group.apps.PassbookPoliciesGroupConfig', - 'passbook.policies.matcher.apps.PassbookPoliciesMatcherConfig', - 'passbook.policies.password.apps.PassbookPoliciesPasswordConfig', - 'passbook.policies.sso.apps.PassbookPoliciesSSOConfig', - 'passbook.policies.webhook.apps.PassbookPoliciesWebhookConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.postgres", + "rest_framework", + "drf_yasg", + "guardian", + "django_prometheus", + "passbook.core.apps.PassbookCoreConfig", + "passbook.admin.apps.PassbookAdminConfig", + "passbook.api.apps.PassbookAPIConfig", + "passbook.lib.apps.PassbookLibConfig", + "passbook.audit.apps.PassbookAuditConfig", + "passbook.recovery.apps.PassbookRecoveryConfig", + "passbook.sources.saml.apps.PassbookSourceSAMLConfig", + "passbook.sources.ldap.apps.PassbookSourceLDAPConfig", + "passbook.sources.oauth.apps.PassbookSourceOAuthConfig", + "passbook.providers.app_gw.apps.PassbookApplicationApplicationGatewayConfig", + "passbook.providers.oauth.apps.PassbookProviderOAuthConfig", + "passbook.providers.oidc.apps.PassbookProviderOIDCConfig", + "passbook.providers.saml.apps.PassbookProviderSAMLConfig", + "passbook.factors.otp.apps.PassbookFactorOTPConfig", + "passbook.factors.captcha.apps.PassbookFactorCaptchaConfig", + "passbook.factors.password.apps.PassbookFactorPasswordConfig", + "passbook.factors.dummy.apps.PassbookFactorDummyConfig", + "passbook.factors.email.apps.PassbookFactorEmailConfig", + "passbook.policies.expiry.apps.PassbookPolicyExpiryConfig", + "passbook.policies.reputation.apps.PassbookPolicyReputationConfig", + "passbook.policies.hibp.apps.PassbookPolicyHIBPConfig", + "passbook.policies.group.apps.PassbookPoliciesGroupConfig", + "passbook.policies.matcher.apps.PassbookPoliciesMatcherConfig", + "passbook.policies.password.apps.PassbookPoliciesPasswordConfig", + "passbook.policies.sso.apps.PassbookPoliciesSSOConfig", + "passbook.policies.webhook.apps.PassbookPoliciesWebhookConfig", ] GUARDIAN_MONKEY_PATCH = False SWAGGER_SETTINGS = { - 'DEFAULT_INFO': 'passbook.api.v2.urls.info', + "DEFAULT_INFO": "passbook.api.v2.urls.info", } REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 100, - 'DEFAULT_FILTER_BACKENDS': [ - 'django_filters.rest_framework.DjangoFilterBackend', - 'rest_framework.filters.OrderingFilter', - 'rest_framework.filters.SearchFilter', + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 100, + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.OrderingFilter", + "rest_framework.filters.SearchFilter", ], - 'DEFAULT_PERMISSION_CLASSES': ( + "DEFAULT_PERMISSION_CLASSES": ( # 'rest_framework.permissions.IsAuthenticated', - 'passbook.api.permissions.CustomObjectPermissions', + "passbook.api.permissions.CustomObjectPermissions", ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.SessionAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ), } @@ -131,11 +127,11 @@ REST_FRAMEWORK = { CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379" - f"/{CONFIG.y('redis.cache_db')}"), - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } + "LOCATION": ( + f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379" + f"/{CONFIG.y('redis.cache_db')}" + ), + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",}, } } DJANGO_REDIS_IGNORE_EXCEPTIONS = True @@ -144,47 +140,47 @@ SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" MIDDLEWARE = [ - 'django_prometheus.middleware.PrometheusBeforeMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django_prometheus.middleware.PrometheusAfterMiddleware', + "django_prometheus.middleware.PrometheusBeforeMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_prometheus.middleware.PrometheusAfterMiddleware", ] -ROOT_URLCONF = 'passbook.root.urls' +ROOT_URLCONF = "passbook.root.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'passbook.root.wsgi.application' +WSGI_APPLICATION = "passbook.root.wsgi.application" # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django_prometheus.db.backends.postgresql', - 'HOST': CONFIG.y('postgresql.host'), - 'NAME': CONFIG.y('postgresql.name'), - 'USER': CONFIG.y('postgresql.user'), - 'PASSWORD': CONFIG.y('postgresql.password'), + "default": { + "ENGINE": "django_prometheus.db.backends.postgresql", + "HOST": CONFIG.y("postgresql.host"), + "NAME": CONFIG.y("postgresql.name"), + "USER": CONFIG.y("postgresql.user"), + "PASSWORD": CONFIG.y("postgresql.password"), } } @@ -193,26 +189,20 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, ] # Internationalization # https://docs.djangoproject.com/en/2.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -225,58 +215,59 @@ USE_TZ = True # Add a 10 minute timeout to all Celery tasks. CELERY_TASK_SOFT_TIME_LIMIT = 600 CELERY_BEAT_SCHEDULE = { - 'clean_nonces': { - 'task': 'passbook.core.tasks.clean_nonces', - 'schedule': crontab(minute='*/5') # Run every 5 minutes + "clean_nonces": { + "task": "passbook.core.tasks.clean_nonces", + "schedule": crontab(minute="*/5"), # Run every 5 minutes } } CELERY_CREATE_MISSING_QUEUES = True -CELERY_TASK_DEFAULT_QUEUE = 'passbook' -CELERY_BROKER_URL = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}" - f":6379/{CONFIG.y('redis.message_queue_db')}") -CELERY_RESULT_BACKEND = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}" - f":6379/{CONFIG.y('redis.message_queue_db')}") +CELERY_TASK_DEFAULT_QUEUE = "passbook" +CELERY_BROKER_URL = ( + f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}" + f":6379/{CONFIG.y('redis.message_queue_db')}" +) +CELERY_RESULT_BACKEND = ( + f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}" + f":6379/{CONFIG.y('redis.message_queue_db')}" +) # Database backup -if CONFIG.y('postgresql.backup'): - INSTALLED_APPS += ['dbbackup'] - DBBACKUP_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' +if CONFIG.y("postgresql.backup"): + INSTALLED_APPS += ["dbbackup"] + DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" DBBACKUP_CONNECTORS = { - 'default': { - 'CONNECTOR': 'dbbackup.db.postgresql.PgDumpConnector' - } + "default": {"CONNECTOR": "dbbackup.db.postgresql.PgDumpConnector"} } - AWS_ACCESS_KEY_ID = CONFIG.y('postgresql.backup.access_key') - AWS_SECRET_ACCESS_KEY = CONFIG.y('postgresql.backup.secret_key') - AWS_STORAGE_BUCKET_NAME = CONFIG.y('postgresql.backup.bucket') - AWS_S3_ENDPOINT_URL = CONFIG.y('postgresql.backup.host') + AWS_ACCESS_KEY_ID = CONFIG.y("postgresql.backup.access_key") + AWS_SECRET_ACCESS_KEY = CONFIG.y("postgresql.backup.secret_key") + AWS_STORAGE_BUCKET_NAME = CONFIG.y("postgresql.backup.bucket") + AWS_S3_ENDPOINT_URL = CONFIG.y("postgresql.backup.host") AWS_DEFAULT_ACL = None - LOGGER.info('Database backup is configured', host=CONFIG.y('postgresql.backup.host')) + LOGGER.info( + "Database backup is configured", host=CONFIG.y("postgresql.backup.host") + ) # Add automatic task to backup - CELERY_BEAT_SCHEDULE['db_backup'] = { - 'task': 'passbook.lib.tasks.backup_database', - 'schedule': crontab(minute=0, hour=0) # Run every day, midnight + CELERY_BEAT_SCHEDULE["db_backup"] = { + "task": "passbook.lib.tasks.backup_database", + "schedule": crontab(minute=0, hour=0), # Run every day, midnight } # Sentry integration -_ERROR_REPORTING = CONFIG.y_bool('error_reporting', False) +_ERROR_REPORTING = CONFIG.y_bool("error_reporting", False) if not DEBUG and _ERROR_REPORTING: LOGGER.info("Error reporting is enabled.") sentry_init( dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3", - integrations=[ - DjangoIntegration(), - CeleryIntegration() - ], + integrations=[DjangoIntegration(), CeleryIntegration()], send_default_pii=True, before_send=before_send, - release='passbook@%s' % __version__ + release="passbook@%s" % __version__, ) # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" structlog.configure_once( @@ -302,9 +293,9 @@ LOG_PRE_CHAIN = [ ] LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { + "version": 1, + "disable_existing_loggers": False, + "formatters": { "plain": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(sort_keys=True), @@ -316,67 +307,70 @@ LOGGING = { "foreign_pre_chain": LOG_PRE_CHAIN, }, }, - 'handlers': { - 'console': { - 'level': DEBUG, - 'class': 'logging.StreamHandler', - 'formatter': "colored" if DEBUG else "plain", + "handlers": { + "console": { + "level": DEBUG, + "class": "logging.StreamHandler", + "formatter": "colored" if DEBUG else "plain", }, }, - 'loggers': { - } + "loggers": {}, } _LOGGING_HANDLER_MAP = { - 'passbook': 'DEBUG', - 'django': 'WARNING', - 'celery': 'WARNING', - 'grpc': 'DEBUG', - 'oauthlib': 'DEBUG', - 'oauth2_provider': 'DEBUG', - 'oidc_provider': 'DEBUG', + "passbook": "DEBUG", + "django": "WARNING", + "celery": "WARNING", + "grpc": "DEBUG", + "oauthlib": "DEBUG", + "oauth2_provider": "DEBUG", + "oidc_provider": "DEBUG", } for handler_name, level in _LOGGING_HANDLER_MAP.items(): - LOGGING['loggers'][handler_name] = { - 'handlers': ['console'], - 'level': level, - 'propagate': True, + LOGGING["loggers"][handler_name] = { + "handlers": ["console"], + "level": level, + "propagate": True, } TEST = False -TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' +TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner" TEST_OUTPUT_VERBOSE = 2 -TEST_OUTPUT_FILE_NAME = 'unittest.xml' +TEST_OUTPUT_FILE_NAME = "unittest.xml" -if any('test' in arg for arg in sys.argv): +if any("test" in arg for arg in sys.argv): LOGGING = None TEST = True CELERY_TASK_ALWAYS_EAGER = True _DISALLOWED_ITEMS = [ - 'INSTALLED_APPS', - 'MIDDLEWARE', - 'AUTHENTICATION_BACKENDS', - 'CELERY_BEAT_SCHEDULE' + "INSTALLED_APPS", + "MIDDLEWARE", + "AUTHENTICATION_BACKENDS", + "CELERY_BEAT_SCHEDULE", ] # Load subapps's INSTALLED_APPS for _app in INSTALLED_APPS: - if _app.startswith('passbook'): - if 'apps' in _app: - _app = '.'.join(_app.split('.')[:-2]) + if _app.startswith("passbook"): + if "apps" in _app: + _app = ".".join(_app.split(".")[:-2]) try: app_settings = importlib.import_module("%s.settings" % _app) - INSTALLED_APPS.extend(getattr(app_settings, 'INSTALLED_APPS', [])) - MIDDLEWARE.extend(getattr(app_settings, 'MIDDLEWARE', [])) - AUTHENTICATION_BACKENDS.extend(getattr(app_settings, 'AUTHENTICATION_BACKENDS', [])) - CELERY_BEAT_SCHEDULE.update(getattr(app_settings, 'CELERY_BEAT_SCHEDULE', {})) + INSTALLED_APPS.extend(getattr(app_settings, "INSTALLED_APPS", [])) + MIDDLEWARE.extend(getattr(app_settings, "MIDDLEWARE", [])) + AUTHENTICATION_BACKENDS.extend( + getattr(app_settings, "AUTHENTICATION_BACKENDS", []) + ) + CELERY_BEAT_SCHEDULE.update( + getattr(app_settings, "CELERY_BEAT_SCHEDULE", {}) + ) for _attr in dir(app_settings): - if not _attr.startswith('__') and _attr not in _DISALLOWED_ITEMS: + if not _attr.startswith("__") and _attr not in _DISALLOWED_ITEMS: globals()[_attr] = getattr(app_settings, _attr) except ImportError: pass if DEBUG: - INSTALLED_APPS.append('debug_toolbar') - MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/passbook/root/urls.py b/passbook/root/urls.py index 44bc5d2ab..2634ebc4e 100644 --- a/passbook/root/urls.py +++ b/passbook/root/urls.py @@ -11,32 +11,34 @@ from passbook.root.monitoring import MetricsView LOGGER = get_logger() admin.autodiscover() -admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login') +admin.site.login = RedirectView.as_view(pattern_name="passbook_core:auth-login") handler400 = error.BadRequestView.as_view() handler403 = error.ForbiddenView.as_view() handler404 = error.NotFoundView.as_view() handler500 = error.ServerErrorView.as_view() -urlpatterns = [ -] +urlpatterns = [] for _passbook_app in get_apps(): - if hasattr(_passbook_app, 'mountpoint'): - _path = path(_passbook_app.mountpoint, include((_passbook_app.name+'.urls', - _passbook_app.label), - namespace=_passbook_app.label)) + if hasattr(_passbook_app, "mountpoint"): + _path = path( + _passbook_app.mountpoint, + include( + (_passbook_app.name + ".urls", _passbook_app.label), + namespace=_passbook_app.label, + ), + ) urlpatterns.append(_path) LOGGER.debug("Mounted URLs", app_name=_passbook_app.name) urlpatterns += [ # Administration - path('administration/django/', admin.site.urls), - path('metrics', MetricsView.as_view(), name='metrics') + path("administration/django/", admin.site.urls), + path("metrics", MetricsView.as_view(), name="metrics"), ] if settings.DEBUG: import debug_toolbar - urlpatterns = [ - path('__debug__/', include(debug_toolbar.urls)), - ] + urlpatterns + + urlpatterns = [path("__debug__/", include(debug_toolbar.urls)),] + urlpatterns diff --git a/passbook/root/wsgi.py b/passbook/root/wsgi.py index 6d5e6788e..6ad62c0a3 100644 --- a/passbook/root/wsgi.py +++ b/passbook/root/wsgi.py @@ -16,35 +16,37 @@ from passbook.lib.utils.http import _get_client_ip_from_meta os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings") + class WSGILogger: """ This is the generalized WSGI middleware for any style request logging. """ def __init__(self, application): self.application = application - self.logger = get_logger('passbook.wsgi') + self.logger = get_logger("passbook.wsgi") def __healthcheck(self, start_response): - start_response('204 OK', []) - return [b''] + start_response("204 OK", []) + return [b""] def __call__(self, environ, start_response): start = time() status_codes = [] content_lengths = [] - if environ.get('HTTP_HOST', '').startswith('kubernetes-healthcheck-host'): + if environ.get("HTTP_HOST", "").startswith("kubernetes-healthcheck-host"): # Don't log kubernetes health/readiness requests return self.__healthcheck(start_response) def custom_start_response(status, response_headers, exc_info=None): - status_codes.append(int(status.partition(' ')[0])) + status_codes.append(int(status.partition(" ")[0])) for name, value in response_headers: - if name.lower() == 'content-length': + if name.lower() == "content-length": content_lengths.append(int(value)) break return start_response(status, response_headers, exc_info) + retval = self.application(environ, custom_start_response) - runtime = int((time() - start) * 10**6) + runtime = int((time() - start) * 10 ** 6) content_length = content_lengths[0] if content_lengths else 0 self.log(status_codes[0], environ, content_length, runtime=runtime) return retval @@ -56,16 +58,18 @@ class WSGILogger: see http://httpd.apache.org/docs/current/mod/mod_log_config.html#formats """ host = _get_client_ip_from_meta(environ) - query_string = '' - if environ.get('QUERY_STRING') != '': + query_string = "" + if environ.get("QUERY_STRING") != "": query_string = f"?{environ.get('QUERY_STRING')}" - self.logger.info(f"{environ.get('PATH_INFO', '')}{query_string}", - host=host, - method=environ.get('REQUEST_METHOD', ''), - protocol=environ.get('SERVER_PROTOCOL', ''), - status=status_code, - size=content_length / 1000 if content_length > 0 else '-', - runtime=kwargs.get('runtime')) + self.logger.info( + f"{environ.get('PATH_INFO', '')}{query_string}", + host=host, + method=environ.get("REQUEST_METHOD", ""), + protocol=environ.get("SERVER_PROTOCOL", ""), + status=status_code, + size=content_length / 1000 if content_length > 0 else "-", + runtime=kwargs.get("runtime"), + ) application = WSGILogger(get_wsgi_application()) diff --git a/passbook/sources/ldap/admin.py b/passbook/sources/ldap/admin.py index 8dd111cd0..0656c92e1 100644 --- a/passbook/sources/ldap/admin.py +++ b/passbook/sources/ldap/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_sources_ldap') +admin_autoregister("passbook_sources_ldap") diff --git a/passbook/sources/ldap/api.py b/passbook/sources/ldap/api.py index 1c8983a9a..50f65f098 100644 --- a/passbook/sources/ldap/api.py +++ b/passbook/sources/ldap/api.py @@ -12,24 +12,22 @@ class LDAPSourceSerializer(ModelSerializer): class Meta: model = LDAPSource fields = SOURCE_SERIALIZER_FIELDS + [ - 'server_uri', - 'bind_cn', - 'bind_password', - 'start_tls', - 'base_dn', - 'additional_user_dn', - 'additional_group_dn', - 'user_object_filter', - 'group_object_filter', - 'user_group_membership_field', - 'object_uniqueness_field', - 'sync_groups', - 'sync_parent_group', - 'property_mappings', + "server_uri", + "bind_cn", + "bind_password", + "start_tls", + "base_dn", + "additional_user_dn", + "additional_group_dn", + "user_object_filter", + "group_object_filter", + "user_group_membership_field", + "object_uniqueness_field", + "sync_groups", + "sync_parent_group", + "property_mappings", ] - extra_kwargs = { - 'bind_password': {'write_only': True} - } + extra_kwargs = {"bind_password": {"write_only": True}} class LDAPPropertyMappingSerializer(ModelSerializer): @@ -37,7 +35,7 @@ class LDAPPropertyMappingSerializer(ModelSerializer): class Meta: model = LDAPPropertyMapping - fields = ['pk', 'name', 'ldap_property', 'object_field'] + fields = ["pk", "name", "ldap_property", "object_field"] class LDAPSourceViewSet(ModelViewSet): diff --git a/passbook/sources/ldap/apps.py b/passbook/sources/ldap/apps.py index 7a56777dc..b755bdd6e 100644 --- a/passbook/sources/ldap/apps.py +++ b/passbook/sources/ldap/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig class PassbookSourceLDAPConfig(AppConfig): """Passbook ldap app config""" - name = 'passbook.sources.ldap' - label = 'passbook_sources_ldap' - verbose_name = 'passbook Sources.LDAP' + name = "passbook.sources.ldap" + label = "passbook_sources_ldap" + verbose_name = "passbook Sources.LDAP" diff --git a/passbook/sources/ldap/auth.py b/passbook/sources/ldap/auth.py index f314e7c07..390a43b84 100644 --- a/passbook/sources/ldap/auth.py +++ b/passbook/sources/ldap/auth.py @@ -14,7 +14,7 @@ class LDAPBackend(ModelBackend): def authenticate(self, request: HttpRequest, **kwargs): """Try to authenticate a user via ldap""" - if 'password' not in kwargs: + if "password" not in kwargs: return None for source in LDAPSource.objects.filter(enabled=True): LOGGER.debug("LDAP Auth attempt", source=source) diff --git a/passbook/sources/ldap/connector.py b/passbook/sources/ldap/connector.py index 57a80d8fe..40904122d 100644 --- a/passbook/sources/ldap/connector.py +++ b/passbook/sources/ldap/connector.py @@ -24,9 +24,12 @@ class Connector: def bind(self): """Bind using Source's Credentials""" - self._connection = ldap3.Connection(self._server, raise_exceptions=True, - user=self._source.bind_cn, - password=self._source.bind_password) + self._connection = ldap3.Connection( + self._server, + raise_exceptions=True, + user=self._source.bind_cn, + password=self._source.bind_password, + ) self._connection.bind() if self._source.start_tls: @@ -35,17 +38,17 @@ class Connector: @staticmethod def encode_pass(password: str) -> bytes: """Encodes a plain-text password so it can be used by AD""" - return '"{}"'.format(password).encode('utf-16-le') + return '"{}"'.format(password).encode("utf-16-le") @property def base_dn_users(self) -> str: """Shortcut to get full base_dn for user lookups""" - return ','.join([self._source.additional_user_dn, self._source.base_dn]) + return ",".join([self._source.additional_user_dn, self._source.base_dn]) @property def base_dn_groups(self) -> str: """Shortcut to get full base_dn for group lookups""" - return ','.join([self._source.additional_group_dn, self._source.base_dn]) + return ",".join([self._source.additional_group_dn, self._source.base_dn]) def sync_groups(self): """Iterate over all LDAP Groups and create passbook_core.Group instances""" @@ -56,22 +59,29 @@ class Connector: search_base=self.base_dn_groups, search_filter=self._source.group_object_filter, search_scope=ldap3.SUBTREE, - attributes=ldap3.ALL_ATTRIBUTES) + attributes=ldap3.ALL_ATTRIBUTES, + ) for group in groups: - attributes = group.get('attributes', {}) + attributes = group.get("attributes", {}) _, created = Group.objects.update_or_create( - attributes__ldap_uniq=attributes.get(self._source.object_uniqueness_field, ''), + attributes__ldap_uniq=attributes.get( + self._source.object_uniqueness_field, "" + ), parent=self._source.sync_parent_group, # defaults=self._build_object_properties(attributes), defaults={ - 'name': attributes.get('name', ''), - 'attributes': { - 'ldap_uniq': attributes.get(self._source.object_uniqueness_field, ''), - 'distinguishedName': attributes.get('distinguishedName') - } - } + "name": attributes.get("name", ""), + "attributes": { + "ldap_uniq": attributes.get( + self._source.object_uniqueness_field, "" + ), + "distinguishedName": attributes.get("distinguishedName"), + }, + }, + ) + LOGGER.debug( + "Synced group", group=attributes.get("name", ""), created=created ) - LOGGER.debug("Synced group", group=attributes.get('name', ''), created=created) def sync_users(self): """Iterate over all LDAP Users and create passbook_core.User instances""" @@ -79,17 +89,22 @@ class Connector: search_base=self.base_dn_users, search_filter=self._source.user_object_filter, search_scope=ldap3.SUBTREE, - attributes=ldap3.ALL_ATTRIBUTES) + attributes=ldap3.ALL_ATTRIBUTES, + ) for user in users: - attributes = user.get('attributes', {}) + attributes = user.get("attributes", {}) user, created = User.objects.update_or_create( - attributes__ldap_uniq=attributes.get(self._source.object_uniqueness_field, ''), + attributes__ldap_uniq=attributes.get( + self._source.object_uniqueness_field, "" + ), defaults=self._build_object_properties(attributes), ) if created: user.set_unusable_password() user.save() - LOGGER.debug("Synced User", user=attributes.get('name', ''), created=created) + LOGGER.debug( + "Synced User", user=attributes.get("name", ""), created=created + ) def sync_membership(self): """Iterate over all Users and assign Groups using memberOf Field""" @@ -99,21 +114,31 @@ class Connector: search_scope=ldap3.SUBTREE, attributes=[ self._source.user_group_membership_field, - self._source.object_uniqueness_field]) + self._source.object_uniqueness_field, + ], + ) group_cache: Dict[str, Group] = {} for user in users: - member_of = user.get('attributes', {}).get(self._source.user_group_membership_field, []) - uniq = user.get('attributes', {}).get(self._source.object_uniqueness_field, []) + member_of = user.get("attributes", {}).get( + self._source.user_group_membership_field, [] + ) + uniq = user.get("attributes", {}).get( + self._source.object_uniqueness_field, [] + ) for group_dn in member_of: # Check if group_dn is within our base_dn_groups, and skip if not if not group_dn.endswith(self.base_dn_groups): continue # Check if we fetched the group already, and if not cache it for later if group_dn not in group_cache: - groups = Group.objects.filter(attributes__distinguishedName=group_dn) + groups = Group.objects.filter( + attributes__distinguishedName=group_dn + ) if not groups.exists(): - LOGGER.warning("Group does not exist in our DB yet, run sync_groups first.", - group=group_dn) + LOGGER.warning( + "Group does not exist in our DB yet, run sync_groups first.", + group=group_dn, + ) return group_cache[group_dn] = groups.first() group = group_cache[group_dn] @@ -124,16 +149,19 @@ class Connector: group.save() LOGGER.debug("Successfully updated group membership") - def _build_object_properties(self, attributes: Dict[str, Any]) -> Dict[str, Dict[Any, Any]]: - properties = { - 'attributes': {} - } + def _build_object_properties( + self, attributes: Dict[str, Any] + ) -> Dict[str, Dict[Any, Any]]: + properties = {"attributes": {}} for mapping in self._source.property_mappings.all().select_subclasses(): - properties[mapping.object_field] = attributes.get(mapping.ldap_property, '') + properties[mapping.object_field] = attributes.get(mapping.ldap_property, "") if self._source.object_uniqueness_field in attributes: - properties['attributes']['ldap_uniq'] = \ - attributes.get(self._source.object_uniqueness_field) - properties['attributes']['distinguishedName'] = attributes.get('distinguishedName') + properties["attributes"]["ldap_uniq"] = attributes.get( + self._source.object_uniqueness_field + ) + properties["attributes"]["distinguishedName"] = attributes.get( + "distinguishedName" + ) return properties def auth_user(self, password: str, **filters: Dict[str, str]) -> Optional[User]: @@ -143,8 +171,10 @@ class Connector: if not users.exists(): return None user: User = users.first() - if 'distinguishedName' not in user.attributes: - LOGGER.debug("User doesn't have DN set, assuming not LDAP imported.", user=user) + if "distinguishedName" not in user.attributes: + LOGGER.debug( + "User doesn't have DN set, assuming not LDAP imported.", user=user + ) return None # Either has unusable password, # or has a password, but couldn't be authenticated by ModelBackend. @@ -165,9 +195,12 @@ class Connector: # Try to bind as new user LOGGER.debug("Attempting Binding as user", user=user) try: - temp_connection = ldap3.Connection(self._server, - user=user.attributes.get('distinguishedName'), - password=password, raise_exceptions=True) + temp_connection = ldap3.Connection( + self._server, + user=user.attributes.get("distinguishedName"), + password=password, + raise_exceptions=True, + ) temp_connection.bind() return user except ldap3.core.exceptions.LDAPInvalidCredentialsResult as exception: diff --git a/passbook/sources/ldap/forms.py b/passbook/sources/ldap/forms.py index 180985e9c..6846c9e81 100644 --- a/passbook/sources/ldap/forms.py +++ b/passbook/sources/ldap/forms.py @@ -15,43 +15,43 @@ class LDAPSourceForm(forms.ModelForm): model = LDAPSource fields = SOURCE_FORM_FIELDS + [ - 'server_uri', - 'bind_cn', - 'bind_password', - 'start_tls', - 'base_dn', - 'additional_user_dn', - 'additional_group_dn', - 'user_object_filter', - 'group_object_filter', - 'user_group_membership_field', - 'object_uniqueness_field', - 'sync_groups', - 'sync_parent_group', - 'property_mappings', + "server_uri", + "bind_cn", + "bind_password", + "start_tls", + "base_dn", + "additional_user_dn", + "additional_group_dn", + "user_object_filter", + "group_object_filter", + "user_group_membership_field", + "object_uniqueness_field", + "sync_groups", + "sync_parent_group", + "property_mappings", ] widgets = { - 'name': forms.TextInput(), - 'server_uri': forms.TextInput(), - 'bind_cn': forms.TextInput(), - 'bind_password': forms.TextInput(), - 'base_dn': forms.TextInput(), - 'additional_user_dn': forms.TextInput(), - 'additional_group_dn': forms.TextInput(), - 'user_object_filter': forms.TextInput(), - 'group_object_filter': forms.TextInput(), - 'user_group_membership_field': forms.TextInput(), - 'object_uniqueness_field': forms.TextInput(), - 'policies': FilteredSelectMultiple(_('policies'), False), - 'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False) + "name": forms.TextInput(), + "server_uri": forms.TextInput(), + "bind_cn": forms.TextInput(), + "bind_password": forms.TextInput(), + "base_dn": forms.TextInput(), + "additional_user_dn": forms.TextInput(), + "additional_group_dn": forms.TextInput(), + "user_object_filter": forms.TextInput(), + "group_object_filter": forms.TextInput(), + "user_group_membership_field": forms.TextInput(), + "object_uniqueness_field": forms.TextInput(), + "policies": FilteredSelectMultiple(_("policies"), False), + "property_mappings": FilteredSelectMultiple(_("Property Mappings"), False), } labels = { - 'server_uri': _('Server URI'), - 'bind_cn': _('Bind CN'), - 'start_tls': _('Enable Start TLS'), - 'base_dn': _('Base DN'), - 'additional_user_dn': _('Addition User DN'), - 'additional_group_dn': _('Addition Group DN'), + "server_uri": _("Server URI"), + "bind_cn": _("Bind CN"), + "start_tls": _("Enable Start TLS"), + "base_dn": _("Base DN"), + "additional_user_dn": _("Addition User DN"), + "additional_group_dn": _("Addition Group DN"), } @@ -61,9 +61,9 @@ class LDAPPropertyMappingForm(forms.ModelForm): class Meta: model = LDAPPropertyMapping - fields = ['name', 'ldap_property', 'object_field'] + fields = ["name", "ldap_property", "object_field"] widgets = { - 'name': forms.TextInput(), - 'ldap_property': forms.TextInput(), - 'object_field': forms.TextInput(), + "name": forms.TextInput(), + "ldap_property": forms.TextInput(), + "object_field": forms.TextInput(), } diff --git a/passbook/sources/ldap/migrations/0001_initial.py b/passbook/sources/ldap/migrations/0001_initial.py index 4cd29ff4b..f73d4018d 100644 --- a/passbook/sources/ldap/migrations/0001_initial.py +++ b/passbook/sources/ldap/migrations/0001_initial.py @@ -10,42 +10,87 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ("passbook_core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='LDAPPropertyMapping', + name="LDAPPropertyMapping", fields=[ - ('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')), - ('ldap_property', models.TextField()), - ('object_field', models.TextField()), + ( + "propertymapping_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.PropertyMapping", + ), + ), + ("ldap_property", models.TextField()), + ("object_field", models.TextField()), ], - options={ - 'abstract': False, - }, - bases=('passbook_core.propertymapping',), + options={"abstract": False,}, + bases=("passbook_core.propertymapping",), ), migrations.CreateModel( - name='LDAPSource', + name="LDAPSource", fields=[ - ('source_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Source')), - ('server_uri', models.URLField(validators=[django.core.validators.URLValidator(schemes=['ldap', 'ldaps'])])), - ('bind_cn', models.TextField()), - ('bind_password', models.TextField()), - ('start_tls', models.BooleanField(default=False)), - ('base_dn', models.TextField()), - ('additional_user_dn', models.TextField(help_text='Prepended to Base DN for User-queries.')), - ('additional_group_dn', models.TextField(help_text='Prepended to Base DN for Group-queries.')), - ('user_object_filter', models.TextField()), - ('group_object_filter', models.TextField()), - ('sync_groups', models.BooleanField(default=True)), - ('sync_parent_group', models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Group')), + ( + "source_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Source", + ), + ), + ( + "server_uri", + models.URLField( + validators=[ + django.core.validators.URLValidator( + schemes=["ldap", "ldaps"] + ) + ] + ), + ), + ("bind_cn", models.TextField()), + ("bind_password", models.TextField()), + ("start_tls", models.BooleanField(default=False)), + ("base_dn", models.TextField()), + ( + "additional_user_dn", + models.TextField( + help_text="Prepended to Base DN for User-queries." + ), + ), + ( + "additional_group_dn", + models.TextField( + help_text="Prepended to Base DN for Group-queries." + ), + ), + ("user_object_filter", models.TextField()), + ("group_object_filter", models.TextField()), + ("sync_groups", models.BooleanField(default=True)), + ( + "sync_parent_group", + models.ForeignKey( + blank=True, + default=None, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="passbook_core.Group", + ), + ), ], options={ - 'verbose_name': 'LDAP Source', - 'verbose_name_plural': 'LDAP Sources', + "verbose_name": "LDAP Source", + "verbose_name_plural": "LDAP Sources", }, - bases=('passbook_core.source',), + bases=("passbook_core.source",), ), ] diff --git a/passbook/sources/ldap/migrations/0002_auto_20191011_0825.py b/passbook/sources/ldap/migrations/0002_auto_20191011_0825.py index 9d7467347..61c83945a 100644 --- a/passbook/sources/ldap/migrations/0002_auto_20191011_0825.py +++ b/passbook/sources/ldap/migrations/0002_auto_20191011_0825.py @@ -6,12 +6,15 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_ldap', '0001_initial'), + ("passbook_sources_ldap", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='ldappropertymapping', - options={'verbose_name': 'LDAP Property Mapping', 'verbose_name_plural': 'LDAP Property Mappings'}, + name="ldappropertymapping", + options={ + "verbose_name": "LDAP Property Mapping", + "verbose_name_plural": "LDAP Property Mappings", + }, ), ] diff --git a/passbook/sources/ldap/migrations/0003_auto_20191011_0825.py b/passbook/sources/ldap/migrations/0003_auto_20191011_0825.py index 4331d7e2e..57347eb2c 100644 --- a/passbook/sources/ldap/migrations/0003_auto_20191011_0825.py +++ b/passbook/sources/ldap/migrations/0003_auto_20191011_0825.py @@ -5,28 +5,28 @@ from django.db import migrations def create_default_ad_property_mappings(apps: Apps, schema_editor): - LDAPPropertyMapping = apps.get_model('passbook_sources_ldap', 'LDAPPropertyMapping') + LDAPPropertyMapping = apps.get_model("passbook_sources_ldap", "LDAPPropertyMapping") mapping = { - 'name': 'name', - 'givenName': 'first_name', - 'sn': 'last_name', - 'sAMAccountName': 'username', - 'mail': 'email' + "name": "name", + "givenName": "first_name", + "sn": "last_name", + "sAMAccountName": "username", + "mail": "email", } for ldap_property, object_field in mapping.items(): LDAPPropertyMapping.objects.get_or_create( ldap_property=ldap_property, object_field=object_field, defaults={ - 'name': f"Autogenerated LDAP Mapping: {ldap_property} -> {object_field}" - }) + "name": f"Autogenerated LDAP Mapping: {ldap_property} -> {object_field}" + }, + ) + class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_ldap', '0002_auto_20191011_0825'), + ("passbook_sources_ldap", "0002_auto_20191011_0825"), ] - operations = [ - migrations.RunPython(create_default_ad_property_mappings) - ] + operations = [migrations.RunPython(create_default_ad_property_mappings)] diff --git a/passbook/sources/ldap/migrations/0004_auto_20191011_0839.py b/passbook/sources/ldap/migrations/0004_auto_20191011_0839.py index f1260c2fc..e4222aae7 100644 --- a/passbook/sources/ldap/migrations/0004_auto_20191011_0839.py +++ b/passbook/sources/ldap/migrations/0004_auto_20191011_0839.py @@ -8,18 +8,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_ldap', '0003_auto_20191011_0825'), + ("passbook_sources_ldap", "0003_auto_20191011_0825"), ] operations = [ migrations.AlterField( - model_name='ldapsource', - name='server_uri', - field=models.TextField(validators=[django.core.validators.URLValidator(schemes=['ldap', 'ldaps'])]), + model_name="ldapsource", + name="server_uri", + field=models.TextField( + validators=[ + django.core.validators.URLValidator(schemes=["ldap", "ldaps"]) + ] + ), ), migrations.AlterField( - model_name='ldapsource', - name='sync_parent_group', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Group'), + model_name="ldapsource", + name="sync_parent_group", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="passbook_core.Group", + ), ), ] diff --git a/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py b/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py index dce40c4dc..396ad5f1b 100644 --- a/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py +++ b/passbook/sources/ldap/migrations/0005_auto_20191011_1059.py @@ -6,28 +6,39 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_ldap', '0004_auto_20191011_0839'), + ("passbook_sources_ldap", "0004_auto_20191011_0839"), ] operations = [ migrations.AddField( - model_name='ldapsource', - name='object_uniqueness_field', - field=models.TextField(default='objectSid', help_text='Field which contains a unique Identifier.'), + model_name="ldapsource", + name="object_uniqueness_field", + field=models.TextField( + default="objectSid", + help_text="Field which contains a unique Identifier.", + ), ), migrations.AddField( - model_name='ldapsource', - name='user_group_membership_field', - field=models.TextField(default='memberOf', help_text='Field which contains Groups of user.'), + model_name="ldapsource", + name="user_group_membership_field", + field=models.TextField( + default="memberOf", help_text="Field which contains Groups of user." + ), ), migrations.AlterField( - model_name='ldapsource', - name='group_object_filter', - field=models.TextField(default='(objectCategory=Group)', help_text='Consider Objects matching this filter to be Groups.'), + model_name="ldapsource", + name="group_object_filter", + field=models.TextField( + default="(objectCategory=Group)", + help_text="Consider Objects matching this filter to be Groups.", + ), ), migrations.AlterField( - model_name='ldapsource', - name='user_object_filter', - field=models.TextField(default='(objectCategory=Person)', help_text='Consider Objects matching this filter to be Users.'), + model_name="ldapsource", + name="user_object_filter", + field=models.TextField( + default="(objectCategory=Person)", + help_text="Consider Objects matching this filter to be Users.", + ), ), ] diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index 88acc16d3..4e91546cd 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -10,34 +10,45 @@ from passbook.core.models import Group, PropertyMapping, Source class LDAPSource(Source): """LDAP Authentication source""" - server_uri = models.TextField(validators=[URLValidator(schemes=['ldap', 'ldaps'])]) + server_uri = models.TextField(validators=[URLValidator(schemes=["ldap", "ldaps"])]) bind_cn = models.TextField() bind_password = models.TextField() start_tls = models.BooleanField(default=False) base_dn = models.TextField() - additional_user_dn = models.TextField(help_text=_('Prepended to Base DN for User-queries.')) - additional_group_dn = models.TextField(help_text=_('Prepended to Base DN for Group-queries.')) + additional_user_dn = models.TextField( + help_text=_("Prepended to Base DN for User-queries.") + ) + additional_group_dn = models.TextField( + help_text=_("Prepended to Base DN for Group-queries.") + ) - user_object_filter = models.TextField(default="(objectCategory=Person)", help_text=_( - 'Consider Objects matching this filter to be Users.')) - user_group_membership_field = models.TextField(default="memberOf", help_text=_( - "Field which contains Groups of user.")) - group_object_filter = models.TextField(default="(objectCategory=Group)", help_text=_( - 'Consider Objects matching this filter to be Groups.')) - object_uniqueness_field = models.TextField(default="objectSid", help_text=_( - 'Field which contains a unique Identifier.')) + user_object_filter = models.TextField( + default="(objectCategory=Person)", + help_text=_("Consider Objects matching this filter to be Users."), + ) + user_group_membership_field = models.TextField( + default="memberOf", help_text=_("Field which contains Groups of user.") + ) + group_object_filter = models.TextField( + default="(objectCategory=Group)", + help_text=_("Consider Objects matching this filter to be Groups."), + ) + object_uniqueness_field = models.TextField( + default="objectSid", help_text=_("Field which contains a unique Identifier.") + ) sync_groups = models.BooleanField(default=True) - sync_parent_group = models.ForeignKey(Group, blank=True, null=True, - default=None, on_delete=models.SET_DEFAULT) + sync_parent_group = models.ForeignKey( + Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT + ) - form = 'passbook.sources.ldap.forms.LDAPSourceForm' + form = "passbook.sources.ldap.forms.LDAPSourceForm" class Meta: - verbose_name = _('LDAP Source') - verbose_name_plural = _('LDAP Sources') + verbose_name = _("LDAP Source") + verbose_name_plural = _("LDAP Sources") class LDAPPropertyMapping(PropertyMapping): @@ -46,12 +57,12 @@ class LDAPPropertyMapping(PropertyMapping): ldap_property = models.TextField() object_field = models.TextField() - form = 'passbook.sources.ldap.forms.LDAPPropertyMappingForm' + form = "passbook.sources.ldap.forms.LDAPPropertyMappingForm" def __str__(self): return f"LDAP Property Mapping {self.ldap_property} -> {self.object_field}" class Meta: - verbose_name = _('LDAP Property Mapping') - verbose_name_plural = _('LDAP Property Mappings') + verbose_name = _("LDAP Property Mapping") + verbose_name_plural = _("LDAP Property Mappings") diff --git a/passbook/sources/ldap/settings.py b/passbook/sources/ldap/settings.py index 88492c4bf..392332f1d 100644 --- a/passbook/sources/ldap/settings.py +++ b/passbook/sources/ldap/settings.py @@ -2,12 +2,12 @@ from celery.schedules import crontab AUTHENTICATION_BACKENDS = [ - 'passbook.sources.ldap.auth.LDAPBackend', + "passbook.sources.ldap.auth.LDAPBackend", ] CELERY_BEAT_SCHEDULE = { - 'sync': { - 'task': 'passbook.sources.ldap.tasks.sync', - 'schedule': crontab(minute=0) # Run every hour + "sync": { + "task": "passbook.sources.ldap.tasks.sync", + "schedule": crontab(minute=0), # Run every hour } } diff --git a/passbook/sources/ldap/tasks.py b/passbook/sources/ldap/tasks.py index 138d5ff17..581d27c7a 100644 --- a/passbook/sources/ldap/tasks.py +++ b/passbook/sources/ldap/tasks.py @@ -12,6 +12,7 @@ def sync_groups(source_pk: int): connector.bind() connector.sync_groups() + @CELERY_APP.task() def sync_users(source_pk: int): """Sync LDAP Users on background worker""" @@ -20,6 +21,7 @@ def sync_users(source_pk: int): connector.bind() connector.sync_users() + @CELERY_APP.task() def sync(): """Sync all sources""" diff --git a/passbook/sources/oauth/admin.py b/passbook/sources/oauth/admin.py index 50cec0a49..bd9ad0587 100644 --- a/passbook/sources/oauth/admin.py +++ b/passbook/sources/oauth/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_sources_oauth') +admin_autoregister("passbook_sources_oauth") diff --git a/passbook/sources/oauth/api.py b/passbook/sources/oauth/api.py index 0b0060210..4f6c909a6 100644 --- a/passbook/sources/oauth/api.py +++ b/passbook/sources/oauth/api.py @@ -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): diff --git a/passbook/sources/oauth/apps.py b/passbook/sources/oauth/apps.py index 1672ed344..a784cb22a 100644 --- a/passbook/sources/oauth/apps.py +++ b/passbook/sources/oauth/apps.py @@ -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""" diff --git a/passbook/sources/oauth/backends.py b/passbook/sources/oauth/backends.py index 055aa8a69..ee5d0a8a0 100644 --- a/passbook/sources/oauth/backends.py +++ b/passbook/sources/oauth/backends.py @@ -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: diff --git a/passbook/sources/oauth/clients.py b/passbook/sources/oauth/clients.py index 79cde0695..da247bb46 100644 --- a/passbook/sources/oauth/clients.py +++ b/passbook/sources/oauth/clients.py @@ -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: diff --git a/passbook/sources/oauth/forms.py b/passbook/sources/oauth/forms.py index bb6b3b3e6..0f9dfefde 100644 --- a/passbook/sources/oauth/forms.py +++ b/passbook/sources/oauth/forms.py @@ -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", } diff --git a/passbook/sources/oauth/migrations/0001_initial.py b/passbook/sources/oauth/migrations/0001_initial.py index 6e61b2815..bf801c749 100644 --- a/passbook/sources/oauth/migrations/0001_initial.py +++ b/passbook/sources/oauth/migrations/0001_initial.py @@ -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",), ), ] diff --git a/passbook/sources/oauth/models.py b/passbook/sources/oauth/models.py index 21826271e..165f24321 100644 --- a/passbook/sources/oauth/models.py +++ b/passbook/sources/oauth/models.py @@ -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:
%s
" % \ - reverse_lazy('passbook_sources_oauth:oauth-client-callback', - kwargs={'source_slug': self.slug}) + return "Callback URL:
%s
" % 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") diff --git a/passbook/sources/oauth/settings.py b/passbook/sources/oauth/settings.py index 2158129e0..e7cf7d06a 100644 --- a/passbook/sources/oauth/settings.py +++ b/passbook/sources/oauth/settings.py @@ -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", ] diff --git a/passbook/sources/oauth/types/azure_ad.py b/passbook/sources/oauth/types/azure_ad.py index f9d2cdafe..1af72f40e 100644 --- a/passbook/sources/oauth/types/azure_ad.py +++ b/passbook/sources/oauth/types/azure_ad.py @@ -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) diff --git a/passbook/sources/oauth/types/discord.py b/passbook/sources/oauth/types/discord.py index 9361d44c0..d775824cf 100644 --- a/passbook/sources/oauth/types/discord.py +++ b/passbook/sources/oauth/types/discord.py @@ -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 diff --git a/passbook/sources/oauth/types/facebook.py b/passbook/sources/oauth/types/facebook.py index 0f6fd3a9c..ab21a2e79 100644 --- a/passbook/sources/oauth/types/facebook.py +++ b/passbook/sources/oauth/types/facebook.py @@ -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 diff --git a/passbook/sources/oauth/types/github.py b/passbook/sources/oauth/types/github.py index bfa0580dc..efaa232b6 100644 --- a/passbook/sources/oauth/types/github.py +++ b/passbook/sources/oauth/types/github.py @@ -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 diff --git a/passbook/sources/oauth/types/google.py b/passbook/sources/oauth/types/google.py index b244359b3..4c721f438 100644 --- a/passbook/sources/oauth/types/google.py +++ b/passbook/sources/oauth/types/google.py @@ -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 diff --git a/passbook/sources/oauth/types/manager.py b/passbook/sources/oauth/types/manager.py index be4d366f7..deed4cb1a 100644 --- a/passbook/sources/oauth/types/manager.py +++ b/passbook/sources/oauth/types/manager.py @@ -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): diff --git a/passbook/sources/oauth/types/reddit.py b/passbook/sources/oauth/types/reddit.py index 0b349bbf7..830f6c3b9 100644 --- a/passbook/sources/oauth/types/reddit.py +++ b/passbook/sources/oauth/types/reddit.py @@ -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 diff --git a/passbook/sources/oauth/types/supervisr.py b/passbook/sources/oauth/types/supervisr.py index 7f3670b1e..153dd68ab 100644 --- a/passbook/sources/oauth/types/supervisr.py +++ b/passbook/sources/oauth/types/supervisr.py @@ -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 diff --git a/passbook/sources/oauth/types/twitter.py b/passbook/sources/oauth/types/twitter.py index b43a0cfc5..60d74a64c 100644 --- a/passbook/sources/oauth/types/twitter.py +++ b/passbook/sources/oauth/types/twitter.py @@ -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 diff --git a/passbook/sources/oauth/urls.py b/passbook/sources/oauth/urls.py index b15f27314..c4dde11df 100644 --- a/passbook/sources/oauth/urls.py +++ b/passbook/sources/oauth/urls.py @@ -6,12 +6,24 @@ from passbook.sources.oauth.types.manager import RequestKind from passbook.sources.oauth.views import core, dispatcher, user urlpatterns = [ - path('login//', dispatcher.DispatcherView.as_view( - kind=RequestKind.redirect), name='oauth-client-login'), - path('callback//', dispatcher.DispatcherView.as_view( - kind=RequestKind.callback), name='oauth-client-callback'), - path('disconnect//', core.DisconnectView.as_view(), - name='oauth-client-disconnect'), - path('user//', user.UserSettingsView.as_view(), - name='oauth-client-user'), + path( + "login//", + dispatcher.DispatcherView.as_view(kind=RequestKind.redirect), + name="oauth-client-login", + ), + path( + "callback//", + dispatcher.DispatcherView.as_view(kind=RequestKind.callback), + name="oauth-client-callback", + ), + path( + "disconnect//", + core.DisconnectView.as_view(), + name="oauth-client-disconnect", + ), + path( + "user//", + user.UserSettingsView.as_view(), + name="oauth-client-user", + ), ] diff --git a/passbook/sources/oauth/utils.py b/passbook/sources/oauth/utils.py index ed5bd3e0e..a6d19b2e2 100644 --- a/passbook/sources/oauth/utils.py +++ b/passbook/sources/oauth/utils.py @@ -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 diff --git a/passbook/sources/oauth/views/core.py b/passbook/sources/oauth/views/core.py index 03f3e6e41..d5db9eae4 100644 --- a/passbook/sources/oauth/views/core.py +++ b/passbook/sources/oauth/views/core.py @@ -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,}, + ), + }, + ) diff --git a/passbook/sources/oauth/views/dispatcher.py b/passbook/sources/oauth/views/dispatcher.py index 6b25b8f9c..a7ce9024d 100644 --- a/passbook/sources/oauth/views/dispatcher.py +++ b/passbook/sources/oauth/views/dispatcher.py @@ -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) diff --git a/passbook/sources/oauth/views/user.py b/passbook/sources/oauth/views/user.py index c47a670c3..f05ab2549 100644 --- a/passbook/sources/oauth/views/user.py +++ b/passbook/sources/oauth/views/user.py @@ -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) diff --git a/passbook/sources/saml/admin.py b/passbook/sources/saml/admin.py index 56bfaeba3..010b830a6 100644 --- a/passbook/sources/saml/admin.py +++ b/passbook/sources/saml/admin.py @@ -2,4 +2,4 @@ from passbook.lib.admin import admin_autoregister -admin_autoregister('passbook_sources_saml') +admin_autoregister("passbook_sources_saml") diff --git a/passbook/sources/saml/api.py b/passbook/sources/saml/api.py index 7af165d0b..b117e56df 100644 --- a/passbook/sources/saml/api.py +++ b/passbook/sources/saml/api.py @@ -11,7 +11,14 @@ class SAMLSourceSerializer(ModelSerializer): class Meta: model = SAMLSource - fields = ['pk', 'entity_id', 'idp_url', 'idp_logout_url', 'auto_logout', 'signing_cert'] + fields = [ + "pk", + "entity_id", + "idp_url", + "idp_logout_url", + "auto_logout", + "signing_cert", + ] class SAMLSourceViewSet(ModelViewSet): diff --git a/passbook/sources/saml/apps.py b/passbook/sources/saml/apps.py index b58fd73fe..d133474cc 100644 --- a/passbook/sources/saml/apps.py +++ b/passbook/sources/saml/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig class PassbookSourceSAMLConfig(AppConfig): """passbook saml_idp app config""" - name = 'passbook.sources.saml' - label = 'passbook_sources_saml' - verbose_name = 'passbook Sources.SAML' - mountpoint = 'source/saml/' + name = "passbook.sources.saml" + label = "passbook_sources_saml" + verbose_name = "passbook Sources.SAML" + mountpoint = "source/saml/" diff --git a/passbook/sources/saml/forms.py b/passbook/sources/saml/forms.py index 6c19568e1..ba4c52760 100644 --- a/passbook/sources/saml/forms.py +++ b/passbook/sources/saml/forms.py @@ -16,22 +16,27 @@ class SAMLSourceForm(forms.ModelForm): super().__init__(*args, **kwargs) builder = CertificateBuilder() builder.build() - self.fields['signing_cert'].initial = builder.certificate + self.fields["signing_cert"].initial = builder.certificate class Meta: model = SAMLSource - fields = SOURCE_FORM_FIELDS + ['entity_id', 'idp_url', - 'idp_logout_url', 'auto_logout', 'signing_cert'] + fields = SOURCE_FORM_FIELDS + [ + "entity_id", + "idp_url", + "idp_logout_url", + "auto_logout", + "signing_cert", + ] labels = { - 'entity_id': 'Entity ID', - 'idp_url': 'IDP URL', - 'idp_logout_url': 'IDP Logout URL', + "entity_id": "Entity ID", + "idp_url": "IDP URL", + "idp_logout_url": "IDP Logout URL", } widgets = { - 'name': forms.TextInput(), - 'policies': FilteredSelectMultiple(_('policies'), False), - 'entity_id': forms.TextInput(), - 'idp_url': forms.TextInput(), - 'idp_logout_url': forms.TextInput(), + "name": forms.TextInput(), + "policies": FilteredSelectMultiple(_("policies"), False), + "entity_id": forms.TextInput(), + "idp_url": forms.TextInput(), + "idp_logout_url": forms.TextInput(), } diff --git a/passbook/sources/saml/migrations/0001_initial.py b/passbook/sources/saml/migrations/0001_initial.py index 380234206..f60041229 100644 --- a/passbook/sources/saml/migrations/0001_initial.py +++ b/passbook/sources/saml/migrations/0001_initial.py @@ -9,25 +9,33 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0005_merge_20191025_2022'), + ("passbook_core", "0005_merge_20191025_2022"), ] operations = [ migrations.CreateModel( - name='SAMLSource', + name="SAMLSource", 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')), - ('acs_url', models.URLField()), - ('slo_url', models.URLField()), - ('entity_id', models.TextField(blank=True, default=None)), - ('idp_url', models.URLField()), - ('auto_logout', models.BooleanField(default=False)), - ('signing_cert', models.TextField()), - ('signing_key', 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", + ), + ), + ("acs_url", models.URLField()), + ("slo_url", models.URLField()), + ("entity_id", models.TextField(blank=True, default=None)), + ("idp_url", models.URLField()), + ("auto_logout", models.BooleanField(default=False)), + ("signing_cert", models.TextField()), + ("signing_key", models.TextField()), ], - options={ - 'abstract': False, - }, - bases=('passbook_core.source',), + options={"abstract": False,}, + bases=("passbook_core.source",), ), ] diff --git a/passbook/sources/saml/migrations/0002_auto_20191107_1505.py b/passbook/sources/saml/migrations/0002_auto_20191107_1505.py index 294c44b40..eb010c9e3 100644 --- a/passbook/sources/saml/migrations/0002_auto_20191107_1505.py +++ b/passbook/sources/saml/migrations/0002_auto_20191107_1505.py @@ -6,20 +6,17 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_saml', '0001_initial'), + ("passbook_sources_saml", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='samlsource', - options={'verbose_name': 'SAML Source', 'verbose_name_plural': 'SAML Sources'}, - ), - migrations.RemoveField( - model_name='samlsource', - name='acs_url', - ), - migrations.RemoveField( - model_name='samlsource', - name='slo_url', + name="samlsource", + options={ + "verbose_name": "SAML Source", + "verbose_name_plural": "SAML Sources", + }, ), + migrations.RemoveField(model_name="samlsource", name="acs_url",), + migrations.RemoveField(model_name="samlsource", name="slo_url",), ] diff --git a/passbook/sources/saml/migrations/0003_auto_20191107_1550.py b/passbook/sources/saml/migrations/0003_auto_20191107_1550.py index 10d065cb7..34eabb76f 100644 --- a/passbook/sources/saml/migrations/0003_auto_20191107_1550.py +++ b/passbook/sources/saml/migrations/0003_auto_20191107_1550.py @@ -6,17 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('passbook_sources_saml', '0002_auto_20191107_1505'), + ("passbook_sources_saml", "0002_auto_20191107_1505"), ] operations = [ - migrations.RemoveField( - model_name='samlsource', - name='signing_key', - ), + migrations.RemoveField(model_name="samlsource", name="signing_key",), migrations.AddField( - model_name='samlsource', - name='idp_logout_url', + model_name="samlsource", + name="idp_logout_url", field=models.URLField(blank=True, default=None, null=True), ), ] diff --git a/passbook/sources/saml/models.py b/passbook/sources/saml/models.py index f28a32bf0..c28474935 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/sources/saml/models.py @@ -15,21 +15,21 @@ class SAMLSource(Source): auto_logout = models.BooleanField(default=False) signing_cert = models.TextField() - form = 'passbook.sources.saml.forms.SAMLSourceForm' + form = "passbook.sources.saml.forms.SAMLSourceForm" @property def login_button(self): - url = reverse_lazy('passbook_sources_saml:login', kwargs={'source': self.slug}) - return url, '', self.name + url = reverse_lazy("passbook_sources_saml:login", kwargs={"source": self.slug}) + return url, "", self.name @property def additional_info(self): - metadata_url = reverse_lazy('passbook_sources_saml:metadata', kwargs={ - 'source': self - }) - return f"Metadata Download" + metadata_url = reverse_lazy( + "passbook_sources_saml:metadata", kwargs={"source": self} + ) + return f'Metadata Download' class Meta: - verbose_name = _('SAML Source') - verbose_name_plural = _('SAML Sources') + verbose_name = _("SAML Source") + verbose_name_plural = _("SAML Sources") diff --git a/passbook/sources/saml/urls.py b/passbook/sources/saml/urls.py index affc03b9a..d5e5ad04d 100644 --- a/passbook/sources/saml/urls.py +++ b/passbook/sources/saml/urls.py @@ -1,12 +1,11 @@ """saml sp urls""" from django.urls import path -from passbook.sources.saml.views import (ACSView, InitiateView, MetadataView, - SLOView) +from passbook.sources.saml.views import ACSView, InitiateView, MetadataView, SLOView urlpatterns = [ - path('/', InitiateView.as_view(), name='login'), - path('/acs/', ACSView.as_view(), name='acs'), - path('/slo/', SLOView.as_view(), name='slo'), - path('/metadata/', MetadataView.as_view(), name='metadata'), + path("/", InitiateView.as_view(), name="login"), + path("/acs/", ACSView.as_view(), name="acs"), + path("/slo/", SLOView.as_view(), name="slo"), + path("/metadata/", MetadataView.as_view(), name="metadata"), ] diff --git a/passbook/sources/saml/utils.py b/passbook/sources/saml/utils.py index 0d90e574b..4bd8b2b7f 100644 --- a/passbook/sources/saml/utils.py +++ b/passbook/sources/saml/utils.py @@ -10,16 +10,15 @@ def get_entity_id(request: HttpRequest, source: SAMLSource): """Get Source's entity ID, falling back to our Metadata URL if none is set""" entity_id = source.entity_id if entity_id is None: - return build_full_url('metadata', request, source) + return build_full_url("metadata", request, source) return entity_id def build_full_url(view: str, request: HttpRequest, source: SAMLSource) -> str: """Build Full ACS URL to be used in IDP""" return request.build_absolute_uri( - reverse(f"passbook_sources_saml:{view}", kwargs={ - 'source': source.slug - })) + reverse(f"passbook_sources_saml:{view}", kwargs={"source": source.slug}) + ) def _get_email_from_response(root): @@ -48,20 +47,22 @@ def _get_attributes_from_response(root): """ flat_attributes = {} assertion = root.find("{urn:oasis:names:tc:SAML:2.0:assertion}Assertion") - attributes = assertion.find("{urn:oasis:names:tc:SAML:2.0:assertion}AttributeStatement") + attributes = assertion.find( + "{urn:oasis:names:tc:SAML:2.0:assertion}AttributeStatement" + ) for attribute in attributes.getchildren(): - name = attribute.attrib.get('Name') + name = attribute.attrib.get("Name") children = attribute.getchildren() if not children: # Ignore empty-valued attributes. (I think these are not allowed.) continue if len(children) == 1: - #See NOTE: + # See NOTE: flat_attributes[name] = children[0].text else: # It has multiple values. for child in children: - #See NOTE: + # See NOTE: flat_attributes.setdefault(name, []).append(child.text) return flat_attributes @@ -76,9 +77,7 @@ def _get_user_from_response(root): try: user = User.objects.get(email=email) except User.DoesNotExist: - user = User.objects.create_user( - username=email, - email=email) + user = User.objects.create_user(username=email, email=email) user.set_unusable_password() user.save() return user diff --git a/passbook/sources/saml/views.py b/passbook/sources/saml/views.py index 392f1848c..085d9b583 100644 --- a/passbook/sources/saml/views.py +++ b/passbook/sources/saml/views.py @@ -13,8 +13,11 @@ from passbook.providers.saml.base import get_random_id, get_time_string from passbook.providers.saml.utils import nice64 from passbook.providers.saml.views import render_xml from passbook.sources.saml.models import SAMLSource -from passbook.sources.saml.utils import (_get_user_from_response, - build_full_url, get_entity_id) +from passbook.sources.saml.utils import ( + _get_user_from_response, + build_full_url, + get_entity_id, +) from passbook.sources.saml.xml_render import get_authnrequest_xml @@ -26,26 +29,30 @@ class InitiateView(View): source: SAMLSource = get_object_or_404(SAMLSource, slug=source) if not source.enabled: raise Http404 - sso_destination = request.GET.get('next', None) - request.session['sso_destination'] = sso_destination + sso_destination = request.GET.get("next", None) + request.session["sso_destination"] = sso_destination parameters = { - 'ACS_URL': build_full_url('acs', request, source), - 'DESTINATION': source.idp_url, - 'AUTHN_REQUEST_ID': get_random_id(), - 'ISSUE_INSTANT': get_time_string(), - 'ISSUER': get_entity_id(request, source), + "ACS_URL": build_full_url("acs", request, source), + "DESTINATION": source.idp_url, + "AUTHN_REQUEST_ID": get_random_id(), + "ISSUE_INSTANT": get_time_string(), + "ISSUER": get_entity_id(request, source), } authn_req = get_authnrequest_xml(parameters, signed=False) _request = nice64(str.encode(authn_req)) - return render(request, 'saml/sp/login.html', { - 'request_url': source.idp_url, - 'request': _request, - 'token': sso_destination, - 'source': source - }) + return render( + request, + "saml/sp/login.html", + { + "request_url": source.idp_url, + "request": _request, + "token": sso_destination, + "source": source, + }, + ) -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class ACSView(View): """AssertionConsumerService, consume assertion and log user in""" @@ -55,13 +62,13 @@ class ACSView(View): if not source.enabled: raise Http404 # sso_session = request.POST.get('RelayState', None) - data = request.POST.get('SAMLResponse', None) + data = request.POST.get("SAMLResponse", None) response = base64.b64decode(data) root = ElementTree.fromstring(response) user = _get_user_from_response(root) # attributes = _get_attributes_from_response(root) - login(request, user, backend='django.contrib.auth.backends.ModelBackend') - return redirect(reverse('passbook_core:overview')) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") + return redirect(reverse("passbook_core:overview")) class SLOView(View): @@ -73,10 +80,14 @@ class SLOView(View): if not source.enabled: raise Http404 logout(request) - return render(request, 'saml/sp/sso_single_logout.html', { - 'idp_logout_url': source.idp_logout_url, - 'autosubmit': source.auto_logout, - }) + return render( + request, + "saml/sp/sso_single_logout.html", + { + "idp_logout_url": source.idp_logout_url, + "autosubmit": source.auto_logout, + }, + ) class MetadataView(View): @@ -86,8 +97,12 @@ class MetadataView(View): """Replies with the XML Metadata SPSSODescriptor.""" source: SAMLSource = get_object_or_404(SAMLSource, slug=source) entity_id = get_entity_id(request, source) - return render_xml(request, 'saml/sp/xml/spssodescriptor.xml', { - 'acs_url': build_full_url('acs', request, source), - 'entity_id': entity_id, - 'cert_public_key': source.signing_cert, - }) + return render_xml( + request, + "saml/sp/xml/spssodescriptor.xml", + { + "acs_url": build_full_url("acs", request, source), + "entity_id": entity_id, + "cert_public_key": source.signing_cert, + }, + ) diff --git a/passbook/sources/saml/xml_render.py b/passbook/sources/saml/xml_render.py index b438cfec3..899105f2b 100644 --- a/passbook/sources/saml/xml_render.py +++ b/passbook/sources/saml/xml_render.py @@ -12,17 +12,17 @@ def get_authnrequest_xml(parameters, signed=False): # Reset signature. params = {} params.update(parameters) - params['AUTHN_REQUEST_SIGNATURE'] = '' + params["AUTHN_REQUEST_SIGNATURE"] = "" - unsigned = render_to_string('saml/sp/xml/authn_request.xml', params) - LOGGER.debug('AuthN Request', unsigned=unsigned) + unsigned = render_to_string("saml/sp/xml/authn_request.xml", params) + LOGGER.debug("AuthN Request", unsigned=unsigned) if not signed: return unsigned # Sign it. signature_xml = get_signature_xml() - params['AUTHN_REQUEST_SIGNATURE'] = signature_xml - signed = render_to_string('saml/sp/xml/authn_request.xml', params) + params["AUTHN_REQUEST_SIGNATURE"] = signature_xml + signed = render_to_string("saml/sp/xml/authn_request.xml", params) - LOGGER.debug('AuthN Request', signed=signed) + LOGGER.debug("AuthN Request", signed=signed) return signed diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 075ee4976..7e5a6e3b9 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -1,5 +1,5 @@ #!/bin/bash -xe +black passbook scripts/coverage.sh -isort pylint passbook prospector