From 67aa4aef11d238f129be232a6389146882a64f44 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 20 Mar 2019 20:03:28 +0100 Subject: [PATCH 1/9] add modal for OAuth Providers showing the URLs --- .../administration/provider/list.html | 4 ++ .../admin/templatetags/admin_reflection.py | 23 +++++++++ passbook/oauth_provider/models.py | 15 ++++++ .../oauth2_provider/setup_url_modal.html | 49 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html diff --git a/passbook/admin/templates/administration/provider/list.html b/passbook/admin/templates/administration/provider/list.html index 0887b0000..ace453870 100644 --- a/passbook/admin/templates/administration/provider/list.html +++ b/passbook/admin/templates/administration/provider/list.html @@ -57,6 +57,10 @@ {% trans name %} {% endfor %} + {% get_htmls provider as htmls %} + {% for html in htmls %} + {{ html|safe }} + {% endfor %} {% endfor %} diff --git a/passbook/admin/templatetags/admin_reflection.py b/passbook/admin/templatetags/admin_reflection.py index 8f3c112e6..7d4cbe302 100644 --- a/passbook/admin/templatetags/admin_reflection.py +++ b/passbook/admin/templatetags/admin_reflection.py @@ -5,6 +5,8 @@ from logging import getLogger from django import template from django.db.models import Model +from passbook.lib.utils.template import render_to_string + register = template.Library() LOGGER = getLogger(__name__) @@ -29,3 +31,24 @@ def get_links(model_instance): pass return links + + +@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_' + htmls = [] + + if not isinstance(model_instance, Model): + LOGGER.warning("Model %s is not instance of Model", model_instance) + return htmls + + try: + for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): + if name.startswith(prefix): + template, _context = method(context.get('request')) + htmls.append(render_to_string(template, _context)) + except NotImplementedError: + pass + + return htmls diff --git a/passbook/oauth_provider/models.py b/passbook/oauth_provider/models.py index 8334050d2..a318eb88e 100644 --- a/passbook/oauth_provider/models.py +++ b/passbook/oauth_provider/models.py @@ -1,5 +1,6 @@ """Oauth2 provider product extension""" +from django.shortcuts import reverse from django.utils.translation import gettext as _ from oauth2_provider.models import AbstractApplication @@ -14,6 +15,20 @@ class OAuth2Provider(Provider, AbstractApplication): def __str__(self): return "OAuth2 Provider %s" % 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_oauth_provider:oauth2-authorize')), + 'token_url': request.build_absolute_uri( + reverse('passbook_oauth_provider:token')), + 'userinfo_url': request.build_absolute_uri( + reverse('passbook_api:openid')), + 'openid_url': request.build_absolute_uri( + reverse('passbook_oauth_provider:openid-discovery')) + } + class Meta: verbose_name = _('OAuth2 Provider') diff --git a/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html b/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html new file mode 100644 index 000000000..b17f9ed7f --- /dev/null +++ b/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html @@ -0,0 +1,49 @@ +{% load i18n %} + + + \ No newline at end of file From d6f9b2e47d0445112c31bc89b4d0c9a8ab6b43de Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 20 Mar 2019 20:09:27 +0100 Subject: [PATCH 2/9] remove user field from form. Closes #32 --- passbook/oauth_provider/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passbook/oauth_provider/forms.py b/passbook/oauth_provider/forms.py index a47dd7813..1da6ee820 100644 --- a/passbook/oauth_provider/forms.py +++ b/passbook/oauth_provider/forms.py @@ -11,5 +11,5 @@ class OAuth2ProviderForm(forms.ModelForm): class Meta: model = OAuth2Provider - fields = ['name', 'user', 'redirect_uris', 'client_type', + fields = ['name', 'redirect_uris', 'client_type', 'authorization_grant_type', 'client_id', 'client_secret', ] From 1456ee6d3ee0c7847c634ec778ae015aa19b4e99 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 20 Mar 2019 23:00:22 +0100 Subject: [PATCH 3/9] prepare 0.1.24 --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index c38fa81ef..d60880ad6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +passbook (0.1.24) stable; urgency=medium + + * bump version: 0.1.22-beta -> 0.1.23-beta + * add modal for OAuth Providers showing the URLs + * remove user field from form. Closes #32 + + -- Jens Langhammer Wed, 20 Mar 2019 21:59:21 +0000 + passbook (0.1.23) stable; urgency=medium * add support for OpenID-Connect Discovery From 6f7b917c3866c2bb22538769c51ceba0cec0a856 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 20 Mar 2019 23:00:33 +0100 Subject: [PATCH 4/9] bump version: 0.1.23-beta -> 0.1.24-beta --- .bumpversion.cfg | 2 +- .gitlab-ci.yml | 2 +- client-packages/allauth/setup.py | 2 +- client-packages/sentry-auth-passbook/setup.py | 2 +- helm/passbook/Chart.yaml | 4 ++-- helm/passbook/values.yaml | 2 +- passbook/__init__.py | 2 +- passbook/admin/__init__.py | 2 +- passbook/api/__init__.py | 2 +- passbook/audit/__init__.py | 2 +- passbook/captcha_factor/__init__.py | 2 +- passbook/core/__init__.py | 2 +- passbook/hibp_policy/__init__.py | 2 +- passbook/ldap/__init__.py | 2 +- passbook/lib/__init__.py | 2 +- passbook/oauth_client/__init__.py | 2 +- passbook/oauth_provider/__init__.py | 2 +- passbook/otp/__init__.py | 2 +- passbook/password_expiry_policy/__init__.py | 2 +- passbook/saml_idp/__init__.py | 2 +- 20 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cb13bf647..298cb70e6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.23-beta +current_version = 0.1.24-beta tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P.*) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5de99d710..5688add49 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -54,7 +54,7 @@ package-docker: before_script: - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json script: - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.23-beta + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.24-beta stage: build only: - tags diff --git a/client-packages/allauth/setup.py b/client-packages/allauth/setup.py index 4a23f6dbd..0560181cc 100644 --- a/client-packages/allauth/setup.py +++ b/client-packages/allauth/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup( name='django-allauth-passbook', - version='0.1.23-beta', + version='0.1.24-beta', description='passbook support for django-allauth', # long_description='\n'.join(read_simple('docs/index.md')[2:]), long_description_content_type='text/markdown', diff --git a/client-packages/sentry-auth-passbook/setup.py b/client-packages/sentry-auth-passbook/setup.py index ba78a5c3e..b391a3314 100644 --- a/client-packages/sentry-auth-passbook/setup.py +++ b/client-packages/sentry-auth-passbook/setup.py @@ -18,7 +18,7 @@ tests_require = [ setup( name='sentry-auth-passbook', - version='0.1.23-beta', + version='0.1.24-beta', author='BeryJu.org', author_email='support@beryju.org', url='https://passbook.beryju.org', diff --git a/helm/passbook/Chart.yaml b/helm/passbook/Chart.yaml index 356babbfd..e4eb345ff 100644 --- a/helm/passbook/Chart.yaml +++ b/helm/passbook/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v1 -appVersion: "0.1.23-beta" +appVersion: "0.1.24-beta" description: A Helm chart for passbook. name: passbook -version: "0.1.23-beta" +version: "0.1.24-beta" icon: https://passbook.beryju.org/images/logo.png diff --git a/helm/passbook/values.yaml b/helm/passbook/values.yaml index b11af9c69..ea288d8f9 100644 --- a/helm/passbook/values.yaml +++ b/helm/passbook/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: - tag: 0.1.23-beta + tag: 0.1.24-beta nameOverride: "" diff --git a/passbook/__init__.py b/passbook/__init__.py index 4a442e7f6..57b482b0e 100644 --- a/passbook/__init__.py +++ b/passbook/__init__.py @@ -1,2 +1,2 @@ """passbook""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/admin/__init__.py b/passbook/admin/__init__.py index 2e8e17d43..868c13dd8 100644 --- a/passbook/admin/__init__.py +++ b/passbook/admin/__init__.py @@ -1,2 +1,2 @@ """passbook admin""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/api/__init__.py b/passbook/api/__init__.py index 4052057e9..f08500650 100644 --- a/passbook/api/__init__.py +++ b/passbook/api/__init__.py @@ -1,2 +1,2 @@ """passbook api""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/audit/__init__.py b/passbook/audit/__init__.py index 5d4ee6855..ae96a6525 100644 --- a/passbook/audit/__init__.py +++ b/passbook/audit/__init__.py @@ -1,2 +1,2 @@ """passbook audit Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/captcha_factor/__init__.py b/passbook/captcha_factor/__init__.py index 59032c203..47efcd666 100644 --- a/passbook/captcha_factor/__init__.py +++ b/passbook/captcha_factor/__init__.py @@ -1,2 +1,2 @@ """passbook captcha_factor Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/core/__init__.py b/passbook/core/__init__.py index f7ce1af49..9072b8ea0 100644 --- a/passbook/core/__init__.py +++ b/passbook/core/__init__.py @@ -1,2 +1,2 @@ """passbook core""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/hibp_policy/__init__.py b/passbook/hibp_policy/__init__.py index cabb3b265..76cc6ceba 100644 --- a/passbook/hibp_policy/__init__.py +++ b/passbook/hibp_policy/__init__.py @@ -1,2 +1,2 @@ """passbook hibp_policy""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/ldap/__init__.py b/passbook/ldap/__init__.py index 58969a15c..fd6133bfd 100644 --- a/passbook/ldap/__init__.py +++ b/passbook/ldap/__init__.py @@ -1,2 +1,2 @@ """Passbook ldap app Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/lib/__init__.py b/passbook/lib/__init__.py index e2c7051f9..d2f081f57 100644 --- a/passbook/lib/__init__.py +++ b/passbook/lib/__init__.py @@ -1,2 +1,2 @@ """passbook lib""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/oauth_client/__init__.py b/passbook/oauth_client/__init__.py index de770f6e8..e8bd6f588 100644 --- a/passbook/oauth_client/__init__.py +++ b/passbook/oauth_client/__init__.py @@ -1,2 +1,2 @@ """passbook oauth_client Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/oauth_provider/__init__.py b/passbook/oauth_provider/__init__.py index b5be187ad..225c20e10 100644 --- a/passbook/oauth_provider/__init__.py +++ b/passbook/oauth_provider/__init__.py @@ -1,2 +1,2 @@ """passbook oauth_provider Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/otp/__init__.py b/passbook/otp/__init__.py index 3025d5722..ba8286b42 100644 --- a/passbook/otp/__init__.py +++ b/passbook/otp/__init__.py @@ -1,2 +1,2 @@ """passbook otp Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/password_expiry_policy/__init__.py b/passbook/password_expiry_policy/__init__.py index dd6eb81e8..80439dfa6 100644 --- a/passbook/password_expiry_policy/__init__.py +++ b/passbook/password_expiry_policy/__init__.py @@ -1,2 +1,2 @@ """passbook password_expiry""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' diff --git a/passbook/saml_idp/__init__.py b/passbook/saml_idp/__init__.py index 49c9a7fba..258ef7ece 100644 --- a/passbook/saml_idp/__init__.py +++ b/passbook/saml_idp/__init__.py @@ -1,2 +1,2 @@ """passbook saml_idp Header""" -__version__ = '0.1.23-beta' +__version__ = '0.1.24-beta' From 260c5555fac45931600bd8f4d977c3c7e80b49e7 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2019 11:08:08 +0100 Subject: [PATCH 5/9] add redis dependency back in for caching --- debian/control | 2 +- helm/passbook/charts/redis-5.1.0.tgz | Bin 0 -> 18157 bytes helm/passbook/requirements.lock | 7 +++++-- helm/passbook/requirements.yaml | 3 +++ .../passbook/templates/passbook-configmap.yaml | 1 + passbook/core/requirements.txt | 17 +++++++++-------- passbook/core/settings.py | 12 ++++++++++++ passbook/lib/default.yml | 1 + 8 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 helm/passbook/charts/redis-5.1.0.tgz diff --git a/debian/control b/debian/control index 41f1bc431..24d4909f8 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Standards-Version: 3.9.6 Package: passbook Architecture: all -Recommends: mysql-server, rabbitmq-server +Recommends: mysql-server, rabbitmq-server, redis-server Pre-Depends: adduser, libldap2-dev, libsasl2-dev Depends: python3 (>= 3.5) | python3.6 | python3.7, python3-pip, dbconfig-pgsql | dbconfig-no-thanks, ${misc:Depends} Description: Authentication Provider/Proxy supporting protocols like SAML, OAuth, LDAP and more. diff --git a/helm/passbook/charts/redis-5.1.0.tgz b/helm/passbook/charts/redis-5.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..71237425a5f53c88b1a4b0b0861432278301d24b GIT binary patch literal 18157 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 literal 0 HcmV?d00001 diff --git a/helm/passbook/requirements.lock b/helm/passbook/requirements.lock index e0d0da107..15be0d3d0 100644 --- a/helm/passbook/requirements.lock +++ b/helm/passbook/requirements.lock @@ -5,5 +5,8 @@ dependencies: - name: postgresql repository: https://kubernetes-charts.storage.googleapis.com/ version: 3.10.1 -digest: sha256:c36e054785f7d706d7d3f525eb1b167dbc89b42f84da7fc167a18bbb6542c999 -generated: 2019-03-11T20:36:35.125079+01:00 +- name: redis + repository: https://kubernetes-charts.storage.googleapis.com/ + version: 5.1.0 +digest: sha256:8bf68bc928a2e3c0f05139635be05fa0840554c7bde4cecd624fac78fb5fa5a3 +generated: 2019-03-21T11:06:51.553379+01:00 diff --git a/helm/passbook/requirements.yaml b/helm/passbook/requirements.yaml index 9ae71eac5..0f2e1f356 100644 --- a/helm/passbook/requirements.yaml +++ b/helm/passbook/requirements.yaml @@ -5,3 +5,6 @@ dependencies: - name: postgresql version: 3.10.1 repository: https://kubernetes-charts.storage.googleapis.com/ +- name: redis + version: 5.1.0 + repository: https://kubernetes-charts.storage.googleapis.com/ diff --git a/helm/passbook/templates/passbook-configmap.yaml b/helm/passbook/templates/passbook-configmap.yaml index 8283e8d20..6fcf669a3 100644 --- a/helm/passbook/templates/passbook-configmap.yaml +++ b/helm/passbook/templates/passbook-configmap.yaml @@ -37,6 +37,7 @@ data: secure_proxy_header: HTTP_X_FORWARDED_PROTO: https rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq" + redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master" # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: {{ .Values.config.error_reporting }} diff --git a/passbook/core/requirements.txt b/passbook/core/requirements.txt index dea78258c..e6bf90dbe 100644 --- a/passbook/core/requirements.txt +++ b/passbook/core/requirements.txt @@ -1,12 +1,13 @@ -django>=2.0 -django-model-utils +celery +cherrypy +colorlog django-ipware +django-model-utils +django-redis +django>=2.0 djangorestframework +idna<2.8,>=2.5 +markdown +psycopg2 PyYAML raven -markdown -colorlog -celery -psycopg2 -idna<2.8,>=2.5 -cherrypy diff --git a/passbook/core/settings.py b/passbook/core/settings.py index b5b42ef78..f30af2c77 100644 --- a/passbook/core/settings.py +++ b/passbook/core/settings.py @@ -45,6 +45,8 @@ AUTH_USER_MODEL = 'passbook_core.User' CSRF_COOKIE_NAME = 'passbook_csrf' SESSION_COOKIE_NAME = 'passbook_session' +SESSION_ENGINE = "django.contrib.sessions.backends.cache" +SESSION_CACHE_ALIAS = "default" LANGUAGE_COOKIE_NAME = 'passbook_language' AUTHENTICATION_BACKENDS = [ @@ -99,6 +101,16 @@ REST_FRAMEWORK = { ] } +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://%s" % CONFIG.get('redis'), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/passbook/lib/default.yml b/passbook/lib/default.yml index ab1015d9e..450581a93 100644 --- a/passbook/lib/default.yml +++ b/passbook/lib/default.yml @@ -30,6 +30,7 @@ debug: false secure_proxy_header: HTTP_X_FORWARDED_PROTO: https rabbitmq: guest:guest@localhost/passbook +redis: localhost # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: true secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s From 4645d8353f048fbe45761c7ecd1551213707e402 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2019 11:08:32 +0100 Subject: [PATCH 6/9] utilise cache in PolicyEngine --- passbook/core/policies.py | 54 +++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/passbook/core/policies.py b/passbook/core/policies.py index 1c706f165..eecd6ff63 100644 --- a/passbook/core/policies.py +++ b/passbook/core/policies.py @@ -2,6 +2,7 @@ from logging import getLogger from celery import group +from django.core.cache import cache from ipware import get_client_ip from passbook.core.celery import CELERY_APP @@ -9,6 +10,9 @@ from passbook.core.models import Policy, User LOGGER = getLogger(__name__) +def _cache_key(policy, user): + return "%s#%s" % (policy.uuid, user.pk) + @CELERY_APP.task() def _policy_engine_task(user_pk, policy_pk, **kwargs): """Task wrapper to run policy checking""" @@ -29,58 +33,76 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs): if policy_obj.negate: policy_result = not policy_result LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result) + cache_key = _cache_key(policy_obj, user_obj) + cache.set(cache_key, (policy_obj.action, policy_result, message)) + LOGGER.debug("Cached entry as %s", cache_key) return policy_obj.action, policy_result, message class PolicyEngine: """Orchestrate policy checking, launch tasks and return result""" + __group = None + __cached = None + policies = None - _group = None - _request = None - _user = None + __request = None + __user = None def __init__(self, policies): self.policies = policies - self._request = None - self._user = None + self.__request = None + self.__user = None def for_user(self, user): """Check policies for user""" - self._user = user + self.__user = user return self def with_request(self, request): """Set request""" - self._request = request + self.__request = request return self def build(self): """Build task group""" - if not self._user: + if not self.__user: raise ValueError("User not set.") signatures = [] + cached_policies = [] kwargs = { - '__password__': getattr(self._user, '__password__', None), + '__password__': getattr(self.__user, '__password__', None), } - if self._request: - kwargs['remote_ip'], _ = get_client_ip(self._request) + if self.__request: + kwargs['remote_ip'], _ = get_client_ip(self.__request) if not kwargs['remote_ip']: kwargs['remote_ip'] = '255.255.255.255' for policy in self.policies: - signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs)) - self._group = group(signatures)() + cached_policy = cache.get(_cache_key(policy, self.__user), None) + if cached_policy: + LOGGER.debug("Taking result from cache for %s", policy.pk.hex) + cached_policies.append(cached_policy) + else: + LOGGER.debug("Evaluating policy %s", policy.pk.hex) + signatures.append(_policy_engine_task.s(self.__user.pk, policy.pk.hex, **kwargs)) + # If all policies are cached, we have an empty list here. + if signatures: + self.__group = group(signatures)() + self.__cached = cached_policies return self @property def result(self): """Get policy-checking result""" messages = [] + result = [] try: - # ValueError can be thrown from _policy_engine_task when user is None - group_result = self._group.get() + if self.__group: + # ValueError can be thrown from _policy_engine_task when user is None + result += self.__group.get() + result += self.__cached except ValueError as exc: return False, str(exc) - for policy_action, policy_result, policy_message in group_result: + for policy_action, policy_result, policy_message in result: passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \ (policy_action == Policy.ACTION_DENY and not policy_result) LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing) From 0bc6a4fed454d17579486d553153723d1530bb49 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2019 11:28:57 +0100 Subject: [PATCH 7/9] explicitly use redis db --- debian/etc/passbook/config.yml | 2 ++ helm/passbook/templates/passbook-configmap.yaml | 2 +- passbook/lib/default.yml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/debian/etc/passbook/config.yml b/debian/etc/passbook/config.yml index 9fd285802..ad373bd39 100644 --- a/debian/etc/passbook/config.yml +++ b/debian/etc/passbook/config.yml @@ -11,6 +11,8 @@ debug: false secure_proxy_header: HTTP_X_FORWARDED_PROTO: https rabbitmq: guest:guest@localhost/passbook +redis: localhost/0 + # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: true diff --git a/helm/passbook/templates/passbook-configmap.yaml b/helm/passbook/templates/passbook-configmap.yaml index 6fcf669a3..1d6ee3ddd 100644 --- a/helm/passbook/templates/passbook-configmap.yaml +++ b/helm/passbook/templates/passbook-configmap.yaml @@ -37,7 +37,7 @@ data: secure_proxy_header: HTTP_X_FORWARDED_PROTO: https rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq" - redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master" + redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master/0" # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: {{ .Values.config.error_reporting }} diff --git a/passbook/lib/default.yml b/passbook/lib/default.yml index 450581a93..b36d74f65 100644 --- a/passbook/lib/default.yml +++ b/passbook/lib/default.yml @@ -30,7 +30,7 @@ debug: false secure_proxy_header: HTTP_X_FORWARDED_PROTO: https rabbitmq: guest:guest@localhost/passbook -redis: localhost +redis: localhost/0 # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: true secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s From 29913773a7d1197c8693fecceb8fce2859dbb5c2 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2019 11:29:11 +0100 Subject: [PATCH 8/9] invalidate cache when policy is saved --- passbook/core/signals.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/passbook/core/signals.py b/passbook/core/signals.py index 5ca9b243b..53b30cb6f 100644 --- a/passbook/core/signals.py +++ b/passbook/core/signals.py @@ -1,10 +1,15 @@ """passbook core signals""" +from logging import getLogger +from django.core.cache import cache from django.core.signals import Signal +from django.db.models.signals import post_save from django.dispatch import receiver from passbook.core.exceptions import PasswordPolicyInvalid +LOGGER = getLogger(__name__) + user_signed_up = Signal(providing_args=['request', 'user']) invitation_created = Signal(providing_args=['request', 'invitation']) invitation_used = Signal(providing_args=['request', 'invitation', 'user']) @@ -24,3 +29,14 @@ def password_policy_checker(sender, password, **kwargs): passing, messages = policy_engine.result if not passing: raise PasswordPolicyInvalid(*messages) + +@receiver(post_save) +# pylint: disable=unused-argument +def invalidate_policy_cache(sender, instance, **kwargs): + """Invalidate Policy cache when policy is updated""" + from passbook.core.models import Policy + if isinstance(instance, Policy): + LOGGER.debug("Invalidating cache for %s", instance.pk) + keys = cache.keys("%s#*" % instance.pk) + cache.delete_many(keys) + LOGGER.debug("Deleted %d keys", len(keys)) From 87012b65e120c3c25d9af96192608889f223569c Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2019 11:35:40 +0100 Subject: [PATCH 9/9] add redis as service in CI for unittests --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5688add49..a4a7369c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,7 @@ stages: image: python:3.6 services: - postgres:latest + - redis:latest variables: POSTGRES_DB: passbook