From e8000d35d563cb3efe54845d378235770b41c121 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 29 Sep 2022 15:47:18 +0200 Subject: [PATCH 03/10] misc: move hooks module in utils package (#69720) --- src/authentic2/api_views.py | 3 +- src/authentic2/cbv.py | 2 +- src/authentic2/forms/passwords.py | 3 +- src/authentic2/hooks.py | 60 +---------------- src/authentic2/idp/saml/saml2_endpoints.py | 2 +- src/authentic2/manager/role_views.py | 4 +- src/authentic2/manager/user_views.py | 3 +- src/authentic2/manager/views.py | 3 +- src/authentic2/utils/hooks.py | 75 ++++++++++++++++++++++ src/authentic2/utils/misc.py | 2 +- src/authentic2/views.py | 4 +- src/authentic2_auth_fc/views.py | 3 +- src/authentic2_auth_oidc/backends.py | 3 +- src/authentic2_idp_cas/views.py | 2 +- src/authentic2_idp_oidc/utils.py | 3 +- src/authentic2_idp_oidc/views.py | 2 +- tests/conftest.py | 2 +- tests/idp_oidc/test_user_profiles.py | 2 +- 18 files changed, 98 insertions(+), 80 deletions(-) create mode 100644 src/authentic2/utils/hooks.py diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py index b404370c..3606238f 100644 --- a/src/authentic2/api_views.py +++ b/src/authentic2/api_views.py @@ -54,7 +54,7 @@ from rest_framework.viewsets import ModelViewSet, ViewSet from authentic2.compat.drf import action -from . import api_mixins, app_settings, decorators, hooks +from . import api_mixins, app_settings, decorators from .a2_rbac.models import OrganizationalUnit, Role, RoleParenting from .a2_rbac.utils import get_default_ou from .apps.journal.models import Event @@ -62,6 +62,7 @@ from .custom_user.models import Profile, ProfileType, User from .journal_event_types import UserLogin, UserRegistration from .models import APIClient, Attribute, PasswordReset, Service from .passwords import get_password_checker, get_password_strength +from .utils import hooks from .utils import misc as utils_misc from .utils.api import DjangoRBACPermission, NaturalKeyRelatedField from .utils.lookups import Unaccent diff --git a/src/authentic2/cbv.py b/src/authentic2/cbv.py index 6e8a6065..fbf688d5 100644 --- a/src/authentic2/cbv.py +++ b/src/authentic2/cbv.py @@ -20,7 +20,7 @@ from django.forms import Form from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie -from . import hooks +from .utils import hooks from .utils import misc as utils_misc from .utils.views import csrf_token_check diff --git a/src/authentic2/forms/passwords.py b/src/authentic2/forms/passwords.py index 9ae52b50..2512c8b6 100644 --- a/src/authentic2/forms/passwords.py +++ b/src/authentic2/forms/passwords.py @@ -28,8 +28,9 @@ from authentic2.backends.ldap_backend import LDAPUser from authentic2.journal import journal from authentic2.passwords import get_min_password_strength -from .. import app_settings, hooks, models, validators +from .. import app_settings, models, validators from ..backends import get_user_queryset +from ..utils import hooks from ..utils import misc as utils_misc from .fields import CheckPasswordField, NewPasswordField, PasswordField, ValidatedEmailField from .honeypot import HoneypotForm diff --git a/src/authentic2/hooks.py b/src/authentic2/hooks.py index 301a59d5..f21cfd54 100644 --- a/src/authentic2/hooks.py +++ b/src/authentic2/hooks.py @@ -14,62 +14,4 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import logging - -from django.apps import apps -from django.conf import settings - -from . import decorators -from .utils.cache import GlobalCache - - -@GlobalCache -def get_hooks(hook_name): - """Return a list of defined hook named a2_hook on AppConfig classes of installed - Django applications. - - Ordering of hooks can be defined using an orer field on the hook method. - """ - hooks = [] - for app in apps.get_app_configs(): - name = 'a2_hook_' + hook_name - if hasattr(app, name): - hooks.append(getattr(app, name)) - if hasattr(settings, 'A2_HOOKS') and hasattr(settings.A2_HOOKS, 'items'): - v = settings.A2_HOOKS.get(hook_name) - if callable(v): - hooks.append(v) - v = settings.A2_HOOKS.get('__all__') - if callable(v): - hooks.append(lambda *args, **kwargs: v(hook_name, *args, **kwargs)) - hooks.sort(key=lambda hook: getattr(hook, 'order', 0)) - return hooks - - -@decorators.to_list -def call_hooks(hook_name, *args, **kwargs): - '''Call each a2_hook_ and return the list of results.''' - logger = logging.getLogger(__name__) - hooks = get_hooks(hook_name) - for hook in hooks: - try: - yield hook(*args, **kwargs) - except Exception: - if getattr(settings, 'A2_HOOKS_PROPAGATE_EXCEPTIONS', False): - raise - logger.exception('exception while calling hook %s', hook) - - -def call_hooks_first_result(hook_name, *args, **kwargs): - '''Call each a2_hook_ and return the first not None result.''' - logger = logging.getLogger(__name__) - hooks = get_hooks(hook_name) - for hook in hooks: - try: - result = hook(*args, **kwargs) - if result is not None: - return result - except Exception: - if getattr(settings, 'A2_HOOKS_PROPAGATE_EXCEPTIONS', False): - raise - logger.exception('exception while calling hook %s', hook) +from .utils.hooks import call_hooks, call_hooks_first_result, get_hooks # pylint: disable=unused-import diff --git a/src/authentic2/idp/saml/saml2_endpoints.py b/src/authentic2/idp/saml/saml2_endpoints.py index 0ff5a59c..b5476d4e 100644 --- a/src/authentic2/idp/saml/saml2_endpoints.py +++ b/src/authentic2/idp/saml/saml2_endpoints.py @@ -58,7 +58,6 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST -from authentic2 import hooks from authentic2 import views as a2_views from authentic2.attributes_ng.engine import get_attributes from authentic2.compat_lasso import lasso @@ -109,6 +108,7 @@ from authentic2.saml.models import ( saml2_urn_to_nidformat, save_key_values, ) +from authentic2.utils import hooks from authentic2.utils import misc as utils_misc from authentic2.utils.misc import datetime_to_xs_datetime, find_authentication_event from authentic2.utils.misc import get_backends as get_idp_backends diff --git a/src/authentic2/manager/role_views.py b/src/authentic2/manager/role_views.py index 8f64c373..32fbfdf2 100644 --- a/src/authentic2/manager/role_views.py +++ b/src/authentic2/manager/role_views.py @@ -33,12 +33,12 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, FormView, TemplateView from django.views.generic.detail import SingleObjectMixin -from authentic2 import data_transfer, hooks +from authentic2 import data_transfer from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role, RoleParenting from authentic2.a2_rbac.utils import get_default_ou from authentic2.apps.journal.views import JournalViewWithContext from authentic2.forms.profile import modelform_factory -from authentic2.utils import crypto +from authentic2.utils import crypto, hooks from authentic2.utils.misc import redirect from . import forms, resources, tables, views diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py index ca434bb1..54fa0c28 100644 --- a/src/authentic2/manager/user_views.py +++ b/src/authentic2/manager/user_views.py @@ -37,12 +37,11 @@ from django.views.generic import DetailView, FormView, TemplateView, View from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import BaseFormView -from authentic2 import hooks from authentic2.a2_rbac.models import OrganizationalUnit, Role, RoleParenting from authentic2.a2_rbac.utils import get_default_ou from authentic2.apps.journal.views import JournalViewWithContext from authentic2.models import Attribute, PasswordReset -from authentic2.utils import spooler, switch_user +from authentic2.utils import hooks, spooler, switch_user from authentic2.utils.misc import make_url, redirect, select_next_url, send_password_reset_mail from authentic2_idp_oidc.models import OIDCAuthorization, OIDCClient diff --git a/src/authentic2/manager/views.py b/src/authentic2/manager/views.py index c7b39f85..9b29f5d8 100644 --- a/src/authentic2/manager/views.py +++ b/src/authentic2/manager/views.py @@ -39,13 +39,12 @@ from django_select2.views import AutoResponseView from django_tables2 import SingleTableMixin, SingleTableView from gadjo.templatetags.gadjo import xstatic -from authentic2 import hooks from authentic2.a2_rbac.models import OrganizationalUnit from authentic2.backends import ldap_backend from authentic2.data_transfer import ImportContext, export_site, import_site from authentic2.decorators import json as json_view from authentic2.forms.profile import modelform_factory -from authentic2.utils import crypto +from authentic2.utils import crypto, hooks from authentic2.utils.misc import batch_queryset, redirect from . import app_settings, forms, utils, widgets diff --git a/src/authentic2/utils/hooks.py b/src/authentic2/utils/hooks.py new file mode 100644 index 00000000..08fbc278 --- /dev/null +++ b/src/authentic2/utils/hooks.py @@ -0,0 +1,75 @@ +# authentic2 - versatile identity manager +# Copyright (C) 2010-2019 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +from django.apps import apps +from django.conf import settings + +from authentic2 import decorators +from authentic2.utils.cache import GlobalCache + + +@GlobalCache +def get_hooks(hook_name): + """Return a list of defined hook named a2_hook on AppConfig classes of installed + Django applications. + + Ordering of hooks can be defined using an orer field on the hook method. + """ + hooks = [] + for app in apps.get_app_configs(): + name = 'a2_hook_' + hook_name + if hasattr(app, name): + hooks.append(getattr(app, name)) + if hasattr(settings, 'A2_HOOKS') and hasattr(settings.A2_HOOKS, 'items'): + v = settings.A2_HOOKS.get(hook_name) + if callable(v): + hooks.append(v) + v = settings.A2_HOOKS.get('__all__') + if callable(v): + hooks.append(lambda *args, **kwargs: v(hook_name, *args, **kwargs)) + hooks.sort(key=lambda hook: getattr(hook, 'order', 0)) + return hooks + + +@decorators.to_list +def call_hooks(hook_name, *args, **kwargs): + '''Call each a2_hook_ and return the list of results.''' + logger = logging.getLogger(__name__) + hooks = get_hooks(hook_name) + for hook in hooks: + try: + yield hook(*args, **kwargs) + except Exception: + if getattr(settings, 'A2_HOOKS_PROPAGATE_EXCEPTIONS', False): + raise + logger.exception('exception while calling hook %s', hook) + + +def call_hooks_first_result(hook_name, *args, **kwargs): + '''Call each a2_hook_ and return the first not None result.''' + logger = logging.getLogger(__name__) + hooks = get_hooks(hook_name) + for hook in hooks: + try: + result = hook(*args, **kwargs) + if result is not None: + return result + except Exception: + if getattr(settings, 'A2_HOOKS_PROPAGATE_EXCEPTIONS', False): + raise + logger.exception('exception while calling hook %s', hook) diff --git a/src/authentic2/utils/misc.py b/src/authentic2/utils/misc.py index 6e5b4f54..25ce63a5 100644 --- a/src/authentic2/utils/misc.py +++ b/src/authentic2/utils/misc.py @@ -455,7 +455,7 @@ def last_authentication_event(request=None, session=None): def login(request, user, how, nonce=None, record=True, **kwargs): """Login a user model, record the authentication event and redirect to next URL or settings.LOGIN_REDIRECT_URL.""" - from .. import hooks + from . import hooks from .service import get_service from .views import check_cookie_works diff --git a/src/authentic2/views.py b/src/authentic2/views.py index 45f7074b..2596b39c 100644 --- a/src/authentic2/views.py +++ b/src/authentic2/views.py @@ -58,14 +58,14 @@ from authentic2.custom_user.models import iter_attributes from authentic2.forms import authentication as authentication_forms from authentic2_idp_oidc.models import OIDCAuthorization -from . import app_settings, attribute_kinds, cbv, constants, decorators, hooks, models, validators +from . import app_settings, attribute_kinds, cbv, constants, decorators, models, validators from .a2_rbac.models import OrganizationalUnit as OU from .a2_rbac.utils import get_default_ou from .forms import passwords as passwords_forms from .forms import profile as profile_forms from .forms import registration as registration_forms from .models import Lock -from .utils import crypto +from .utils import crypto, hooks from .utils import misc as utils_misc from .utils import switch_user as utils_switch_user from .utils.evaluate import make_condition_context diff --git a/src/authentic2_auth_fc/views.py b/src/authentic2_auth_fc/views.py index 6559dc72..8388ee25 100644 --- a/src/authentic2_auth_fc/views.py +++ b/src/authentic2_auth_fc/views.py @@ -37,10 +37,11 @@ from django.views.generic import FormView, View from requests_oauthlib import OAuth2Session from authentic2 import app_settings as a2_app_settings -from authentic2 import constants, hooks +from authentic2 import constants from authentic2.a2_rbac.utils import get_default_ou from authentic2.forms.passwords import SetPasswordForm from authentic2.models import Attribute, AttributeValue, Lock +from authentic2.utils import hooks from authentic2.utils import misc as utils_misc from authentic2.utils import views as utils_views from authentic2.utils.crypto import check_hmac_url, hash_chain, hmac_url diff --git a/src/authentic2_auth_oidc/backends.py b/src/authentic2_auth_oidc/backends.py index 784e17bb..2069a767 100644 --- a/src/authentic2_auth_oidc/backends.py +++ b/src/authentic2_auth_oidc/backends.py @@ -26,9 +26,10 @@ from django.utils.timezone import now from jwcrypto.jwk import JWK from jwcrypto.jwt import JWT -from authentic2 import app_settings, hooks +from authentic2 import app_settings from authentic2.a2_rbac.models import OrganizationalUnit from authentic2.models import Lock +from authentic2.utils import hooks from authentic2.utils.crypto import base64url_encode from authentic2.utils.template import Template diff --git a/src/authentic2_idp_cas/views.py b/src/authentic2_idp_cas/views.py index cda7dcf1..f1d17a60 100644 --- a/src/authentic2_idp_cas/views.py +++ b/src/authentic2_idp_cas/views.py @@ -25,9 +25,9 @@ from django.http import HttpResponse, HttpResponseBadRequest from django.utils.timezone import now from django.views.generic.base import View -from authentic2 import hooks from authentic2.attributes_ng.engine import get_attributes from authentic2.constants import NONCE_FIELD_NAME +from authentic2.utils import hooks from authentic2.utils.misc import ( attribute_values_to_identifier, find_authentication_event, diff --git a/src/authentic2_idp_oidc/utils.py b/src/authentic2_idp_oidc/utils.py index b7bf9876..ab72d227 100644 --- a/src/authentic2_idp_oidc/utils.py +++ b/src/authentic2_idp_oidc/utils.py @@ -27,9 +27,8 @@ from django.utils.encoding import force_bytes, force_str from jwcrypto.jwk import JWK, InvalidJWKValue, JWKSet from jwcrypto.jwt import JWT -from authentic2 import hooks from authentic2.attributes_ng.engine import get_attributes -from authentic2.utils import crypto +from authentic2.utils import crypto, hooks from authentic2.utils.misc import make_url from authentic2.utils.template import Template diff --git a/src/authentic2_idp_oidc/views.py b/src/authentic2_idp_oidc/views.py index 5d987f3a..8d0fb8c6 100644 --- a/src/authentic2_idp_oidc/views.py +++ b/src/authentic2_idp_oidc/views.py @@ -36,11 +36,11 @@ from django.views.decorators.csrf import csrf_exempt from ratelimit.utils import is_ratelimited from authentic2 import app_settings as a2_app_settings -from authentic2 import hooks from authentic2.a2_rbac.models import OrganizationalUnit from authentic2.custom_user.models import Profile from authentic2.decorators import setting_enabled from authentic2.exponential_retry_timeout import ExponentialRetryTimeout +from authentic2.utils import hooks from authentic2.utils.misc import last_authentication_event, login_require, make_url, redirect from authentic2.utils.service import set_service from authentic2.utils.view_decorators import check_view_restriction diff --git a/tests/conftest.py b/tests/conftest.py index 5d50ca05..ecf6765f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,12 +28,12 @@ from django.core.management import call_command from django.db import connection, transaction from django.db.migrations.executor import MigrationExecutor -from authentic2 import hooks as a2_hooks from authentic2.a2_rbac.models import OrganizationalUnit, Role from authentic2.a2_rbac.utils import get_default_ou from authentic2.authentication import OIDCUser from authentic2.manager.utils import get_ou_count from authentic2.models import Attribute, Service +from authentic2.utils import hooks as a2_hooks from authentic2.utils.evaluate import BaseExpressionValidator from authentic2_auth_oidc.utils import get_provider_by_issuer from authentic2_idp_oidc.models import OIDCClient diff --git a/tests/idp_oidc/test_user_profiles.py b/tests/idp_oidc/test_user_profiles.py index 5b526171..d403b50a 100644 --- a/tests/idp_oidc/test_user_profiles.py +++ b/tests/idp_oidc/test_user_profiles.py @@ -437,7 +437,7 @@ def test_modify_user_info_hook(app, oidc_client, profile_settings, profile_user, token_url = make_url('oidc-token') - with mock.patch('authentic2.hooks.get_hooks') as get_hooks: + with mock.patch('authentic2.utils.hooks.get_hooks') as get_hooks: get_hooks.return_value = mock_get_hooks('') response = app.post( token_url, -- 2.37.2