From 4bb33d3d3c62516cfdb1ff5bba9216936a07d368 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 30 Mar 2021 10:15:50 +0200 Subject: [PATCH 1/6] misc: apply black (#52457) --- debian/debian_config.py | 91 +- debian/multitenant/config.py | 46 +- debian/multitenant/debian_config.py | 14 +- doc/conf.py | 118 +- merge-coverage.py | 399 +- setup.py | 156 +- src/authentic2/a2_rbac/admin.py | 37 +- src/authentic2/a2_rbac/app_settings.py | 1 + src/authentic2/a2_rbac/apps.py | 28 +- src/authentic2/a2_rbac/fields.py | 10 +- src/authentic2/a2_rbac/management.py | 48 +- src/authentic2/a2_rbac/managers.py | 65 +- .../a2_rbac/migrations/0001_initial.py | 150 +- ...3_partial_unique_index_on_name_and_slug.py | 12 +- .../migrations/0004_auto_20150523_0028.py | 6 +- .../migrations/0005_auto_20150526_1406.py | 9 +- .../migrations/0008_auto_20150810_1953.py | 6 +- ...0009_partial_unique_index_on_permission.py | 10 +- .../migrations/0010_auto_20160209_1417.py | 27 +- .../migrations/0014_auto_20170711_1024.py | 6 +- .../migrations/0016_auto_20171208_1429.py | 6 +- ...nizationalunit_user_add_password_policy.py | 6 +- .../0020_partial_unique_index_on_name.py | 11 +- .../migrations/0021_auto_20200317_1514.py | 8 +- .../migrations/0022_auto_20200402_1101.py | 22 +- .../migrations/0024_fix_self_admin_perm.py | 4 +- src/authentic2/a2_rbac/models.py | 239 +- src/authentic2/a2_rbac/signal_handlers.py | 22 +- src/authentic2/a2_rbac/utils.py | 14 +- src/authentic2/admin.py | 91 +- src/authentic2/api_mixins.py | 5 +- src/authentic2/api_urls.py | 14 +- src/authentic2/api_views.py | 435 +- src/authentic2/app.py | 4 +- src/authentic2/app_settings.py | 330 +- .../apps/journal/migrations/0001_initial.py | 2 +- src/authentic2/apps/journal/models.py | 12 +- src/authentic2/apps/journal/search_engine.py | 8 +- .../migrations/0001_initial.py | 411 +- .../migrations/0002_auto_20150409_1840.py | 15 +- .../migrations/0003_auto_20150526_2239.py | 8 +- src/authentic2/attribute_kinds.py | 19 +- src/authentic2/attributes_ng/engine.py | 36 +- .../attributes_ng/sources/__init__.py | 5 +- .../attributes_ng/sources/django_user.py | 4 +- .../attributes_ng/sources/format.py | 11 +- .../attributes_ng/sources/function.py | 10 +- src/authentic2/attributes_ng/sources/ldap.py | 4 +- .../attributes_ng/sources/service_roles.py | 12 +- .../auth2_ssl/migrations/0001_initial.py | 10 +- .../auth_migrations_18/0001_initial.py | 89 +- .../0002_auto_20150323_1720.py | 12 +- .../0003_auto_20150410_1657.py | 2 +- .../auth_migrations_18/0004_user.py | 81 +- .../0005_auto_20150526_2303.py | 24 +- src/authentic2/authentication.py | 25 +- src/authentic2/authenticators.py | 22 +- src/authentic2/backends/ldap_backend.py | 475 +- src/authentic2/backends/models_backend.py | 9 +- src/authentic2/cbv.py | 40 +- src/authentic2/compat/cookies.py | 1 + src/authentic2/compat/misc.py | 6 + src/authentic2/compat_lasso.py | 2 + src/authentic2/context_processors.py | 4 +- src/authentic2/cors.py | 2 - src/authentic2/crypto.py | 27 +- src/authentic2/csv_import.py | 140 +- src/authentic2/custom_user/apps.py | 44 +- .../management/commands/changepassword.py | 8 +- .../management/commands/fix-attributes.py | 7 +- src/authentic2/custom_user/managers.py | 61 +- .../custom_user/migrations/0001_initial.py | 137 +- .../migrations/0002_auto_20150410_1823.py | 32 +- .../migrations/0003_auto_20150504_1410.py | 5 +- .../custom_user/migrations/0004_user_ou.py | 4 +- .../migrations/0005_auto_20150522_1527.py | 8 +- .../migrations/0006_auto_20150527_1212.py | 6 +- .../migrations/0007_auto_20150610_1527.py | 6 +- .../migrations/0009_auto_20150810_1953.py | 6 +- .../migrations/0012_user_modified.py | 7 +- .../migrations/0015_auto_20170707_1653.py | 6 +- .../migrations/0017_auto_20200305_1645.py | 7 +- .../migrations/0020_deleteduser.py | 22 +- .../migrations/0022_index_email.py | 2 +- .../migrations/0023_index_username.py | 2 +- .../0024_index_email_by_trigrams.py | 6 +- .../migrations/0026_remove_user_deleted.py | 3 +- src/authentic2/custom_user/models.py | 163 +- src/authentic2/data_transfer.py | 82 +- src/authentic2/decorators.py | 55 +- .../disco_service/disco_responder.py | 16 +- src/authentic2/exponential_retry_timeout.py | 26 +- src/authentic2/forms/authentication.py | 35 +- src/authentic2/forms/fields.py | 20 +- src/authentic2/forms/mixins.py | 3 +- src/authentic2/forms/passwords.py | 22 +- src/authentic2/forms/profile.py | 14 +- src/authentic2/forms/registration.py | 10 +- src/authentic2/forms/widgets.py | 52 +- src/authentic2/hashers.py | 72 +- src/authentic2/hooks.py | 8 +- src/authentic2/idp/interactions.py | 12 +- src/authentic2/idp/migrations/0001_initial.py | 113 +- .../idp/migrations/0002_auto_20150526_2239.py | 24 +- src/authentic2/idp/saml/__init__.py | 23 +- src/authentic2/idp/saml/app_settings.py | 29 +- src/authentic2/idp/saml/backend.py | 87 +- src/authentic2/idp/saml/saml2_endpoints.py | 431 +- src/authentic2/idp/saml/urls.py | 31 +- src/authentic2/idp/saml/views.py | 3 +- src/authentic2/idp/urls.py | 3 +- src/authentic2/journal_event_types.py | 6 +- src/authentic2/log_filters.py | 7 +- src/authentic2/logger.py | 6 +- .../management/commands/check-and-repair.py | 168 +- .../commands/clean-unused-accounts.py | 13 +- .../deactivate-orphaned-ldap-users.py | 1 - .../management/commands/export_site.py | 5 +- .../management/commands/import_site.py | 22 +- .../management/commands/load-ldif.py | 43 +- .../management/commands/resetpassword.py | 16 +- .../management/commands/sync-ldap-users.py | 1 + src/authentic2/manager/app_settings.py | 2 + src/authentic2/manager/apps.py | 5 +- src/authentic2/manager/fields.py | 20 +- src/authentic2/manager/forms.py | 197 +- src/authentic2/manager/journal_event_types.py | 12 +- src/authentic2/manager/ou_views.py | 38 +- src/authentic2/manager/resources.py | 20 +- src/authentic2/manager/role_views.py | 266 +- src/authentic2/manager/service_views.py | 14 +- src/authentic2/manager/tables.py | 68 +- src/authentic2/manager/urls.py | 249 +- src/authentic2/manager/user_import.py | 15 +- src/authentic2/manager/user_views.py | 285 +- src/authentic2/manager/views.py | 181 +- src/authentic2/manager/widgets.py | 9 +- src/authentic2/managers.py | 4 + src/authentic2/middleware.py | 31 +- src/authentic2/migrations/0001_initial.py | 107 +- .../migrations/0003_auto_20150409_1840.py | 12 +- src/authentic2/migrations/0004_service.py | 5 +- src/authentic2/migrations/0005_service_ou.py | 4 +- .../migrations/0006_conditional_slug_index.py | 7 +- .../migrations/0007_auto_20150523_0028.py | 8 +- .../migrations/0008_auto_20160204_1415.py | 4 +- .../migrations/0009_auto_20160211_2247.py | 1 + .../migrations/0012_auto_20160211_2255.py | 4 +- .../migrations/0013_auto_20160211_2258.py | 12 +- .../migrations/0015_auto_20160621_1711.py | 4 +- .../migrations/0018_auto_20170524_0842.py | 17 +- .../migrations/0021_attribute_order.py | 7 +- .../migrations/0022_attribute_scopes.py | 8 +- .../migrations/0024_auto_20190617_1113.py | 3 +- .../migrations/0025_auto_20191009_1047.py | 7 +- src/authentic2/migrations/0026_token.py | 16 +- .../migrations/0027_remove_deleteduser.py | 5 +- .../migrations/0028_trigram_unaccent_index.py | 2 +- .../migrations/0029_auto_20201013_1614.py | 6 +- .../0030_clean_admin_tools_tables.py | 10 +- .../0031_add_search_vector_to_attributes.py | 23 +- src/authentic2/migrations/__init__.py | 26 +- src/authentic2/models.py | 196 +- .../nonce/migrations/0001_initial.py | 11 +- src/authentic2/nonce/utils.py | 89 +- src/authentic2/passwords.py | 37 +- src/authentic2/plugins.py | 33 +- src/authentic2/saml/__init__.py | 1 + src/authentic2/saml/admin.py | 42 +- src/authentic2/saml/admin_views.py | 12 +- src/authentic2/saml/app_settings.py | 4 +- src/authentic2/saml/common.py | 220 +- src/authentic2/saml/fields.py | 25 +- src/authentic2/saml/forms.py | 32 +- src/authentic2/saml/lasso_helper.py | 13 +- .../saml/management/commands/mapping.py | 4235 ++++++++--------- .../saml/management/commands/sync-metadata.py | 157 +- src/authentic2/saml/managers.py | 23 +- .../saml/migrations/0001_initial.py | 562 ++- .../migrations/0002_auto_20150320_1245.py | 8 +- .../0002_ease_federation_migration.py | 15 +- src/authentic2/saml/migrations/0003_merge.py | 3 +- .../migrations/0004_auto_20150410_1438.py | 7 +- ...e_liberty_provider_inherit_from_service.py | 5 +- .../migrations/0006_restore_foreign_keys.py | 3 + .../0007_copy_service_ptr_id_to_old_id.py | 49 +- .../migrations/0008_alter_foreign_keys.py | 16 +- src/authentic2/saml/migrations/0009_auto.py | 18 +- src/authentic2/saml/migrations/0010_auto.py | 4 +- src/authentic2/saml/migrations/0011_auto.py | 28 +- .../migrations/0012_auto_20150526_2239.py | 12 +- .../migrations/0013_auto_20150617_1004.py | 10 +- .../migrations/0014_auto_20150617_1216.py | 1 + .../migrations/0015_auto_20150915_2032.py | 46 +- src/authentic2/saml/models.py | 303 +- src/authentic2/saml/saml2utils.py | 117 +- src/authentic2/saml/shibboleth/utils.py | 1 - src/authentic2/saml/utils.py | 2 - src/authentic2/saml/x509utils.py | 34 +- src/authentic2/serializers.py | 1 + src/authentic2/settings.py | 31 +- src/authentic2/urls.py | 134 +- src/authentic2/user_login_failure.py | 7 +- src/authentic2/utils/__init__.py | 465 +- src/authentic2/utils/evaluate.py | 70 +- src/authentic2/utils/lazy.py | 6 +- src/authentic2/utils/lookups.py | 2 + src/authentic2/utils/service.py | 3 +- src/authentic2/utils/switch_user.py | 2 - src/authentic2/utils/views.py | 19 +- src/authentic2/validators.py | 8 +- src/authentic2/views.py | 487 +- src/authentic2_auth_fc/api_views.py | 8 +- src/authentic2_auth_fc/app_settings.py | 33 +- src/authentic2_auth_fc/apps.py | 15 +- src/authentic2_auth_fc/authenticators.py | 73 +- src/authentic2_auth_fc/backends.py | 14 +- .../migrations/0001_initial.py | 15 +- .../migrations/0002_auto_20200416_1439.py | 4 +- .../migrations/0003_fcaccount_order1.py | 6 +- src/authentic2_auth_fc/models.py | 23 +- src/authentic2_auth_fc/utils.py | 13 +- src/authentic2_auth_fc/views.py | 166 +- src/authentic2_auth_oidc/admin.py | 33 +- src/authentic2_auth_oidc/app_settings.py | 2 + src/authentic2_auth_oidc/apps.py | 36 +- src/authentic2_auth_oidc/authenticators.py | 11 +- src/authentic2_auth_oidc/backends.py | 162 +- .../commands/oidc-register-issuer.py | 45 +- src/authentic2_auth_oidc/managers.py | 2 + .../migrations/0001_initial.py | 114 +- ..._oidcprovider_token_revocation_endpoint.py | 4 +- .../migrations/0004_auto_20171017_1522.py | 10 +- src/authentic2_auth_oidc/models.py | 166 +- src/authentic2_auth_oidc/utils.py | 117 +- src/authentic2_auth_oidc/views.py | 146 +- src/authentic2_auth_saml/adapters.py | 28 +- src/authentic2_auth_saml/app_settings.py | 2 + src/authentic2_auth_saml/apps.py | 14 +- src/authentic2_auth_saml/authenticators.py | 23 +- src/authentic2_auth_saml/backends.py | 5 +- src/authentic2_auth_saml/urls.py | 5 +- src/authentic2_idp_cas/admin.py | 59 +- src/authentic2_idp_cas/app_settings.py | 1 + src/authentic2_idp_cas/apps.py | 1 - src/authentic2_idp_cas/constants.py | 71 +- .../migrations/0001_initial.py | 104 +- .../migrations/0002_auto_20150410_1438.py | 9 +- .../migrations/0003_auto_20150415_2223.py | 9 +- .../migrations/0004_create_services.py | 6 +- .../0005_alter_field_service_ptr.py | 1 + .../migrations/0006_copy_proxy_m2m.py | 6 +- .../migrations/0007_alter_service.py | 22 +- .../migrations/0008_alter_foreign_keys.py | 3 + .../migrations/0009_alter_related_models.py | 9 +- .../0010_copy_service_ptr_id_to_old_id.py | 3 + .../0011_remove_old_id_restore_proxy.py | 8 +- .../0012_copy_service_proxy_to_m2m.py | 6 +- .../0013_delete_model_service_proxy2.py | 2 +- .../migrations/0015_auto_20170406_1825.py | 8 +- src/authentic2_idp_cas/models.py | 33 +- src/authentic2_idp_cas/urls.py | 6 +- src/authentic2_idp_cas/utils.py | 2 +- src/authentic2_idp_cas/views.py | 149 +- src/authentic2_idp_oidc/admin.py | 37 +- src/authentic2_idp_oidc/app_settings.py | 18 +- src/authentic2_idp_oidc/apps.py | 237 +- .../migrations/0001_initial.py | 129 +- .../migrations/0002_auto_20170121_2346.py | 6 +- .../migrations/0003_auto_20170329_1259.py | 7 +- .../migrations/0005_authorization_mode.py | 14 +- .../migrations/0006_auto_20170720_1054.py | 12 +- .../0008_oidcclient_idtoken_duration.py | 6 +- .../migrations/0010_oidcclaim.py | 28 +- .../migrations/0011_auto_20180808_1546.py | 14 +- .../migrations/0012_auto_20200122_2258.py | 14 +- .../migrations/0013_auto_20200630_1007.py | 4 +- src/authentic2_idp_oidc/models.py | 257 +- src/authentic2_idp_oidc/urls.py | 24 +- src/authentic2_idp_oidc/utils.py | 45 +- src/authentic2_idp_oidc/views.py | 362 +- src/django_rbac/apps.py | 19 +- src/django_rbac/backends.py | 55 +- src/django_rbac/context_processors.py | 1 + src/django_rbac/managers.py | 52 +- src/django_rbac/migrations/0001_initial.py | 11 +- ...ionalunit_permission_role_roleparenting.py | 102 +- .../0003_add_max_aggregate_for_postgres.py | 3 +- src/django_rbac/models.py | 167 +- src/django_rbac/signal_handlers.py | 6 +- src/django_rbac/utils.py | 15 +- tests/auth_fc/conftest.py | 34 +- tests/auth_fc/test_auth_fc.py | 7 +- tests/cache_urls.py | 8 +- tests/conftest.py | 171 +- tests/test_a2_rbac.py | 134 +- tests/test_admin.py | 47 +- tests/test_all.py | 249 +- tests/test_api.py | 541 ++- tests/test_attribute_kinds.py | 55 +- tests/test_auth_oidc.py | 223 +- tests/test_auth_saml.py | 39 +- tests/test_commands.py | 94 +- tests/test_concurrency.py | 15 +- tests/test_crypto.py | 8 +- tests/test_csv_import.py | 41 +- tests/test_custom_user.py | 14 +- tests/test_customfields.py | 25 +- tests/test_data_transfer.py | 302 +- tests/test_fields.py | 6 +- tests/test_hashers.py | 14 +- tests/test_idp_cas.py | 310 +- tests/test_idp_oidc.py | 976 ++-- tests/test_idp_saml2.py | 455 +- tests/test_import_export_site_cmd.py | 46 +- tests/test_journal.py | 27 +- tests/test_large_userbase.py | 17 +- tests/test_ldap.py | 1083 +++-- tests/test_login.py | 10 +- tests/test_manager.py | 134 +- tests/test_manager_journal.py | 66 +- tests/test_natural_key.py | 13 +- tests/test_ou_manager.py | 12 +- tests/test_password_reset.py | 1 + tests/test_passwords.py | 5 +- tests/test_profile.py | 91 +- tests/test_registration.py | 99 +- tests/test_role_manager.py | 65 +- tests/test_saml_x509utils.py | 5 +- tests/test_template.py | 12 +- tests/test_user_manager.py | 240 +- tests/test_user_model.py | 8 +- tests/test_utils.py | 15 +- tests/test_utils_evaluate.py | 14 +- tests/test_validators.py | 22 +- tests/test_views.py | 15 +- tests/utils.py | 57 +- tests_rbac/conftest.py | 4 +- tests_rbac/test_rbac.py | 124 +- 339 files changed, 13976 insertions(+), 10737 deletions(-) diff --git a/debian/debian_config.py b/debian/debian_config.py index d1c0cc4c..530d00d9 100644 --- a/debian/debian_config.py +++ b/debian/debian_config.py @@ -27,14 +27,14 @@ LOGGING = { 'disable_existing_loggers': True, 'filters': { 'cleaning': { - '()': 'authentic2.utils.CleanLogMessage', + '()': 'authentic2.utils.CleanLogMessage', }, 'request_context': { - '()': 'authentic2.log_filters.RequestContextFilter', + '()': 'authentic2.log_filters.RequestContextFilter', }, 'force_debug': { '()': 'authentic2.log_filters.ForceDebugFilter', - } + }, }, 'formatters': { 'syslog': { @@ -124,29 +124,29 @@ A2_OPENED_SESSION_COOKIE_SECURE = True def extract_settings_from_environ(): import json from django.core.exceptions import ImproperlyConfigured - global MANAGERS, DATABASES, SENTRY_TRANSPORT, SENTRY_DSN, INSTALLED_APPS, \ - SECURE_PROXY_SSL_HEADER, CACHES, SESSION_ENGINE, LDAP_AUTH_SETTINGS + + global MANAGERS, DATABASES, SENTRY_TRANSPORT, SENTRY_DSN, INSTALLED_APPS, SECURE_PROXY_SSL_HEADER, CACHES, SESSION_ENGINE, LDAP_AUTH_SETTINGS BOOLEAN_ENVS = ( - 'DEBUG', - 'DEBUG_PROPAGATE_EXCEPTIONS', - 'SESSION_EXPIRE_AT_BROWSER_CLOSE', - 'SESSION_COOKIE_SECURE', - 'EMAIL_USE_TLS', - 'USE_X_FORWARDED_HOST', - 'DISCO_SERVICE', - 'DISCO_USE_OF_METADATA', - 'SHOW_DISCO_IN_MD', - 'SSLAUTH_CREATE_USER', - 'PUSH_PROFILE_UPDATES', - 'A2_ACCEPT_EMAIL_AUTHENTICATION', - 'A2_CAN_RESET_PASSWORD', - 'A2_REGISTRATION_CAN_DELETE_ACCOUNT', - 'A2_REGISTRATION_EMAIL_IS_UNIQUE', - 'REGISTRATION_OPEN', - 'A2_AUTH_PASSWORD_ENABLE', - 'SSLAUTH_ENABLE', - 'A2_IDP_SAML2_ENABLE', + 'DEBUG', + 'DEBUG_PROPAGATE_EXCEPTIONS', + 'SESSION_EXPIRE_AT_BROWSER_CLOSE', + 'SESSION_COOKIE_SECURE', + 'EMAIL_USE_TLS', + 'USE_X_FORWARDED_HOST', + 'DISCO_SERVICE', + 'DISCO_USE_OF_METADATA', + 'SHOW_DISCO_IN_MD', + 'SSLAUTH_CREATE_USER', + 'PUSH_PROFILE_UPDATES', + 'A2_ACCEPT_EMAIL_AUTHENTICATION', + 'A2_CAN_RESET_PASSWORD', + 'A2_REGISTRATION_CAN_DELETE_ACCOUNT', + 'A2_REGISTRATION_EMAIL_IS_UNIQUE', + 'REGISTRATION_OPEN', + 'A2_AUTH_PASSWORD_ENABLE', + 'SSLAUTH_ENABLE', + 'A2_IDP_SAML2_ENABLE', ) def to_boolean(name, default=True): @@ -215,12 +215,12 @@ def extract_settings_from_environ(): globals()[path_env] = tuple(os.environ[path_env].split(':')) + tuple(old) INT_ENVS = ( - 'SESSION_COOKIE_AGE', - 'EMAIL_PORT', - 'AUTHENTICATION_EVENT_EXPIRATION', - 'LOCAL_METADATA_CACHE_TIMEOUT', - 'ACCOUNT_ACTIVATION_DAYS', - 'PASSWORD_RESET_TIMEOUT_DAYS', + 'SESSION_COOKIE_AGE', + 'EMAIL_PORT', + 'AUTHENTICATION_EVENT_EXPIRATION', + 'LOCAL_METADATA_CACHE_TIMEOUT', + 'ACCOUNT_ACTIVATION_DAYS', + 'PASSWORD_RESET_TIMEOUT_DAYS', ) def to_int(name, default): @@ -239,17 +239,17 @@ def extract_settings_from_environ(): except ValueError: raise ImproperlyConfigured('environement variable %s must be an integer' % int_env) - ADMINS = () if 'ADMINS' in os.environ: ADMINS = filter(None, os.environ.get('ADMINS').split(':')) - ADMINS = [ admin.split(';') for admin in ADMINS ] + ADMINS = [admin.split(';') for admin in ADMINS] for admin in ADMINS: - assert len(admin) == 2, 'ADMINS setting must be a colon separated list of name and emails separated by a semi-colon' + assert ( + len(admin) == 2 + ), 'ADMINS setting must be a colon separated list of name and emails separated by a semi-colon' assert '@' in admin[1], 'ADMINS setting pairs second value must be emails' MANAGERS = ADMINS - for key in os.environ: if key.startswith('DATABASE_'): prefix, db_key = key.split('_', 1) @@ -271,27 +271,30 @@ def extract_settings_from_environ(): try: import memcache except: - raise ImproperlyConfigured('Python memcache library is not installed, please do: pip install memcache') + raise ImproperlyConfigured( + 'Python memcache library is not installed, please do: pip install memcache' + ) CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', - 'KEY_PREFIX': 'authentic2', - } - } + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'KEY_PREFIX': 'authentic2', + } + } SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # extract any key starting with setting for key in os.environ: if key.startswith('SETTING_'): - setting_key = key[len('SETTING_'):] + setting_key = key[len('SETTING_') :] value = os.environ[key] try: - value = int(value) + value = int(value) except ValueError: - pass + pass globals()[setting_key] = value + extract_settings_from_environ() CONFIG_FILE = '/etc/authentic2/config.py' diff --git a/debian/multitenant/config.py b/debian/multitenant/config.py index d01a16c5..bafc87b3 100644 --- a/debian/multitenant/config.py +++ b/debian/multitenant/config.py @@ -15,56 +15,56 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False -#ADMINS = ( +# ADMINS = ( # # ('User 1', 'watchdog@example.net'), # # ('User 2', 'janitor@example.net'), -#) +# ) # ALLOWED_HOSTS must be correct in production! # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = [ - '*', + '*', ] # Databases # Default: a local database named "authentic" # https://docs.djangoproject.com/en/1.7/ref/settings/#databases # Warning: don't change ENGINE -#DATABASES['default']['NAME'] = 'authentic2_multitenant' -#DATABASES['default']['USER'] = 'authentic-multitenant' -#DATABASES['default']['PASSWORD'] = '******' -#DATABASES['default']['HOST'] = 'localhost' -#DATABASES['default']['PORT'] = '5432' +# DATABASES['default']['NAME'] = 'authentic2_multitenant' +# DATABASES['default']['USER'] = 'authentic-multitenant' +# DATABASES['default']['PASSWORD'] = '******' +# DATABASES['default']['HOST'] = 'localhost' +# DATABASES['default']['PORT'] = '5432' LANGUAGE_CODE = 'fr-fr' TIME_ZONE = 'Europe/Paris' # Sentry / Raven configuration -#RAVEN_CONFIG = { +# RAVEN_CONFIG = { # 'dsn': '', -#} +# } # Email configuration -#EMAIL_SUBJECT_PREFIX = '[authentic] ' -#SERVER_EMAIL = 'root@authentic.example.org' -#DEFAULT_FROM_EMAIL = 'webmaster@authentic.example.org' +# EMAIL_SUBJECT_PREFIX = '[authentic] ' +# SERVER_EMAIL = 'root@authentic.example.org' +# DEFAULT_FROM_EMAIL = 'webmaster@authentic.example.org' # SMTP configuration -#EMAIL_HOST = 'localhost' -#EMAIL_HOST_USER = '' -#EMAIL_HOST_PASSWORD = '' -#EMAIL_PORT = 25 +# EMAIL_HOST = 'localhost' +# EMAIL_HOST_USER = '' +# EMAIL_HOST_PASSWORD = '' +# EMAIL_PORT = 25 # HTTPS Security -#CSRF_COOKIE_SECURE = True -#SESSION_COOKIE_SECURE = True +# CSRF_COOKIE_SECURE = True +# SESSION_COOKIE_SECURE = True # Idp # SAML 2.0 IDP -#A2_IDP_SAML2_ENABLE = False +# A2_IDP_SAML2_ENABLE = False # CAS 1.0 / 2.0 IDP -#A2_IDP_CAS_ENABLE = False +# A2_IDP_CAS_ENABLE = False # Authentifications -#A2_AUTH_PASSWORD_ENABLE = True -#A2_SSLAUTH_ENABLE = False +# A2_AUTH_PASSWORD_ENABLE = True +# A2_SSLAUTH_ENABLE = False diff --git a/debian/multitenant/debian_config.py b/debian/multitenant/debian_config.py index 6b9058a1..34502893 100644 --- a/debian/multitenant/debian_config.py +++ b/debian/multitenant/debian_config.py @@ -21,11 +21,13 @@ TENANT_SETTINGS_LOADERS = ('hobo.multitenant.settings_loaders.Authentic',) + TEN # Add authentic2 hobo agent INSTALLED_APPS = ('hobo.agent.authentic2',) + INSTALLED_APPS -LOGGING['filters'].update({ - 'cleaning': { - '()': 'authentic2.utils.CleanLogMessage', - }, -}) +LOGGING['filters'].update( + { + 'cleaning': { + '()': 'authentic2.utils.CleanLogMessage', + }, + } +) for handler in LOGGING['handlers'].values(): handler.setdefault('filters', []).append('cleaning') @@ -52,7 +54,7 @@ HOBO_ANONYMOUS_SERVICE_USER_CLASS = 'hobo.rest_authentication.AnonymousAuthentic HOBO_SKELETONS_DIR = os.path.join(VAR_DIR, 'skeletons') -CONFIG_FILE='/etc/%s/config.py' % PROJECT_NAME +CONFIG_FILE = '/etc/%s/config.py' % PROJECT_NAME if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE) as fd: exec(fd.read()) diff --git a/doc/conf.py b/doc/conf.py index 1662746a..1413d24e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -16,16 +16,25 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.imgmath', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -34,7 +43,7 @@ templates_path = ['_templates'] source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -54,37 +63,37 @@ release = '2.0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -96,17 +105,17 @@ html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. @@ -115,7 +124,7 @@ html_logo = 'pictures/eo_logo_t.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -124,44 +133,44 @@ html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Authentic2doc' @@ -170,21 +179,18 @@ htmlhelp_basename = 'Authentic2doc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Authentic2.tex', u'Authentic2 Documentation', - u'Entr\'ouvert', 'manual'), + ('index', 'Authentic2.tex', u'Authentic2 Documentation', u'Entr\'ouvert', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -193,32 +199,29 @@ latex_logo = 'pictures/eo_logo.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'authentic2', u'Authentic2 Documentation', - [u'Mikaël Ates'], 1) -] +man_pages = [('index', 'authentic2', u'Authentic2 Documentation', [u'Mikaël Ates'], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -227,18 +230,25 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Authentic2', u'Authentic2 Documentation', u'Mikaël Ates', - 'Authentic2', 'One line description of project.', 'Miscellaneous'), + ( + 'index', + 'Authentic2', + u'Authentic2 Documentation', + u'Mikaël Ates', + 'Authentic2', + 'One line description of project.', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. diff --git a/merge-coverage.py b/merge-coverage.py index 33666664..15577d5f 100755 --- a/merge-coverage.py +++ b/merge-coverage.py @@ -9,35 +9,70 @@ import re from shutil import copyfile from optparse import OptionParser -### This file came from the https://github.com/flow123d/flow123d repo they were nice enough to spend time to write this. +### This file came from the https://github.com/flow123d/flow123d repo they were nice enough to spend time to write this. ### It is copied here for other people to use on its own. # parse arguments -newline = 10*'\t'; -parser = OptionParser(usage="%prog [options] [file1 file2 ... filen]", version="%prog 1.0", - epilog = "If no files are specified all xml files in current directory will be selected. \n" + - "Useful when there is not known precise file name only location") - -parser.add_option("-o", "--output", dest="filename", default="coverage-merged.xml", - help="output file xml name", metavar="FILE") -parser.add_option("-p", "--path", dest="path", default="./", - help="xml location, default current directory", metavar="FILE") -parser.add_option("-l", "--log", dest="loglevel", default="DEBUG", - help="Log level DEBUG, INFO, WARNING, ERROR, CRITICAL") -parser.add_option("-f", "--filteronly", dest="filteronly", default=False, action='store_true', - help="If set all files will be filtered by keep rules otherwise "+ - "all given files will be merged and filtered.") -parser.add_option("-s", "--suffix", dest="suffix", default='', - help="Additional suffix which will be added to filtered files so they original files can be preserved") -parser.add_option("-k", "--keep", dest="packagefilters", default=None, metavar="NAME", action="append", - help="preserves only specific packages. e.g.: " + newline + - "'python merge.py -k src.la.*'" + newline + - "will keep all packgages in folder " + - "src/la/ and all subfolders of this folders. " + newline + - "There can be mutiple rules e.g.:" + newline + - "'python merge.py -k src.la.* -k unit_tests.la.'" + newline + - "Format of the rule is simple dot (.) separated names with wildcard (*) allowed, e.g: " + newline + - "package.subpackage.*") +newline = 10 * '\t' +parser = OptionParser( + usage="%prog [options] [file1 file2 ... filen]", + version="%prog 1.0", + epilog="If no files are specified all xml files in current directory will be selected. \n" + + "Useful when there is not known precise file name only location", +) + +parser.add_option( + "-o", + "--output", + dest="filename", + default="coverage-merged.xml", + help="output file xml name", + metavar="FILE", +) +parser.add_option( + "-p", "--path", dest="path", default="./", help="xml location, default current directory", metavar="FILE" +) +parser.add_option( + "-l", "--log", dest="loglevel", default="DEBUG", help="Log level DEBUG, INFO, WARNING, ERROR, CRITICAL" +) +parser.add_option( + "-f", + "--filteronly", + dest="filteronly", + default=False, + action='store_true', + help="If set all files will be filtered by keep rules otherwise " + + "all given files will be merged and filtered.", +) +parser.add_option( + "-s", + "--suffix", + dest="suffix", + default='', + help="Additional suffix which will be added to filtered files so they original files can be preserved", +) +parser.add_option( + "-k", + "--keep", + dest="packagefilters", + default=None, + metavar="NAME", + action="append", + help="preserves only specific packages. e.g.: " + + newline + + "'python merge.py -k src.la.*'" + + newline + + "will keep all packgages in folder " + + "src/la/ and all subfolders of this folders. " + + newline + + "There can be mutiple rules e.g.:" + + newline + + "'python merge.py -k src.la.* -k unit_tests.la.'" + + newline + + "Format of the rule is simple dot (.) separated names with wildcard (*) allowed, e.g: " + + newline + + "package.subpackage.*", +) (options, args) = parser.parse_args() @@ -45,218 +80,216 @@ parser.add_option("-k", "--keep", dest="packagefilters", default=None, me path = options.path xmlfiles = args loglevel = getattr(logging, options.loglevel.upper()) -finalxml = os.path.join (path, options.filename) +finalxml = os.path.join(path, options.filename) filteronly = options.filteronly filtersuffix = options.suffix packagefilters = options.packagefilters -logging.basicConfig (level=loglevel, format='%(levelname)s %(asctime)s: %(message)s', datefmt='%x %X') - +logging.basicConfig(level=loglevel, format='%(levelname)s %(asctime)s: %(message)s', datefmt='%x %X') if not xmlfiles: - for filename in os.listdir (path): - if not filename.endswith ('.xml'): continue - fullname = os.path.join (path, filename) - if fullname == finalxml: continue - xmlfiles.append (fullname) - - if not xmlfiles: - print('No xml files found!') - sys.exit (1) + for filename in os.listdir(path): + if not filename.endswith('.xml'): + continue + fullname = os.path.join(path, filename) + if fullname == finalxml: + continue + xmlfiles.append(fullname) + + if not xmlfiles: + print('No xml files found!') + sys.exit(1) else: - xmlfiles=[path+filename for filename in xmlfiles] - + xmlfiles = [path + filename for filename in xmlfiles] # constants -PACKAGES_LIST = 'packages/package'; +PACKAGES_LIST = 'packages/package' PACKAGES_ROOT = 'packages' -CLASSES_LIST = 'classes/class'; +CLASSES_LIST = 'classes/class' CLASSES_ROOT = 'classes' -METHODS_LIST = 'methods/method'; +METHODS_LIST = 'methods/method' METHODS_ROOT = 'methods' -LINES_LIST = 'lines/line'; +LINES_LIST = 'lines/line' LINES_ROOT = 'lines' +def merge_xml(xmlfile1, xmlfile2, outputfile): + # parse + xml1 = ET.parse(xmlfile1) + xml2 = ET.parse(xmlfile2) -def merge_xml (xmlfile1, xmlfile2, outputfile): - # parse - xml1 = ET.parse(xmlfile1) - xml2 = ET.parse(xmlfile2) - - # get packages - packages1 = filter_xml(xml1) - packages2 = filter_xml(xml2) + # get packages + packages1 = filter_xml(xml1) + packages2 = filter_xml(xml2) - # find root - packages1root = xml1.find(PACKAGES_ROOT) + # find root + packages1root = xml1.find(PACKAGES_ROOT) + # merge packages + merge(packages1root, packages1, packages2, 'name', merge_packages) - # merge packages - merge (packages1root, packages1, packages2, 'name', merge_packages); + # write result to output file + xml1.write(outputfile, encoding="UTF-8", xml_declaration=True) - # write result to output file - xml1.write (outputfile, encoding="UTF-8", xml_declaration=True) +def filter_xml(xmlfile): + xmlroot = xmlfile.getroot() + packageroot = xmlfile.find(PACKAGES_ROOT) + packages = xmlroot.findall(PACKAGES_LIST) -def filter_xml (xmlfile): - xmlroot = xmlfile.getroot() - packageroot = xmlfile.find(PACKAGES_ROOT) - packages = xmlroot.findall (PACKAGES_LIST) + # delete nodes from tree AND from list + included = [] + if packagefilters: + logging.debug('excluding packages:') + for pckg in packages: + name = pckg.get('name') + if not include_package(name): + logging.debug('excluding package "{0}"'.format(name)) + packageroot.remove(pckg) + else: + included.append(pckg) + return included - # delete nodes from tree AND from list - included = [] - if packagefilters: logging.debug ('excluding packages:') - for pckg in packages: - name = pckg.get('name') - if not include_package (name): - logging.debug ('excluding package "{0}"'.format(name)) - packageroot.remove (pckg) - else: - included.append (pckg) - return included +def prepare_packagefilters(): + if not packagefilters: + return None -def prepare_packagefilters (): - if not packagefilters: - return None + # create simple regexp from given filter + for i in range(len(packagefilters)): + packagefilters[i] = '^' + packagefilters[i].replace('.', '\.').replace('*', '.*') + '$' - # create simple regexp from given filter - for i in range (len (packagefilters)): - packagefilters[i] = '^' + packagefilters[i].replace ('.', '\.').replace ('*', '.*') + '$' +def include_package(name): + if not packagefilters: + return True + for packagefilter in packagefilters: + if re.search(packagefilter, name): + return True + return False -def include_package (name): - if not packagefilters: - return True - for packagefilter in packagefilters: - if re.search(packagefilter, name): - return True - return False +def get_attributes_chain(obj, attrs): + """Return a joined arguments of object based on given arguments""" -def get_attributes_chain (obj, attrs): - """Return a joined arguments of object based on given arguments""" + if type(attrs) is list: + result = '' + for attr in attrs: + result += obj.attrib[attr] + return result + else: + return obj.attrib[attrs] - if type(attrs) is list: - result = '' - for attr in attrs: - result += obj.attrib[attr] - return result - else: - return obj.attrib[attrs] +def merge(root, list1, list2, attr, merge_function): + """Groups given lists based on group attributes. Process of merging items with same key is handled by + passed merge_function. Returns list1.""" + for item2 in list2: + found = False + for item1 in list1: + if get_attributes_chain(item1, attr) == get_attributes_chain(item2, attr): + item1 = merge_function(item1, item2) + found = True + break + if found: + continue + else: + root.append(item2) -def merge (root, list1, list2, attr, merge_function): - """ Groups given lists based on group attributes. Process of merging items with same key is handled by - passed merge_function. Returns list1. """ - for item2 in list2: - found = False - for item1 in list1: - if get_attributes_chain(item1, attr) == get_attributes_chain(item2, attr): - item1 = merge_function (item1, item2) - found = True - break - if found: - continue - else: - root.append(item2) +def merge_packages(package1, package2): + """Merges two packages. Returns package1.""" + classes1 = package1.findall(CLASSES_LIST) + classes2 = package2.findall(CLASSES_LIST) + if classes1 or classes2: + merge(package1.find(CLASSES_ROOT), classes1, classes2, ['filename', 'name'], merge_classes) -def merge_packages (package1, package2): - """Merges two packages. Returns package1.""" - classes1 = package1.findall (CLASSES_LIST); - classes2 = package2.findall (CLASSES_LIST); - if classes1 or classes2: - merge (package1.find (CLASSES_ROOT), classes1, classes2, ['filename','name'], merge_classes); + return package1 - return package1 +def merge_classes(class1, class2): + """Merges two classes. Returns class1.""" -def merge_classes (class1, class2): - """Merges two classes. Returns class1.""" + lines1 = class1.findall(LINES_LIST) + lines2 = class2.findall(LINES_LIST) + if lines1 or lines2: + merge(class1.find(LINES_ROOT), lines1, lines2, 'number', merge_lines) - lines1 = class1.findall (LINES_LIST); - lines2 = class2.findall (LINES_LIST); - if lines1 or lines2: - merge (class1.find (LINES_ROOT), lines1, lines2, 'number', merge_lines); + methods1 = class1.findall(METHODS_LIST) + methods2 = class2.findall(METHODS_LIST) + if methods1 or methods2: + merge(class1.find(METHODS_ROOT), methods1, methods2, 'name', merge_methods) - methods1 = class1.findall (METHODS_LIST) - methods2 = class2.findall (METHODS_LIST) - if methods1 or methods2: - merge (class1.find (METHODS_ROOT), methods1, methods2, 'name', merge_methods); + return class1 - return class1 +def merge_methods(method1, method2): + """Merges two methods. Returns method1.""" -def merge_methods (method1, method2): - """Merges two methods. Returns method1.""" + lines1 = method1.findall(LINES_LIST) + lines2 = method2.findall(LINES_LIST) + merge(method1.find(LINES_ROOT), lines1, lines2, 'number', merge_lines) - lines1 = method1.findall (LINES_LIST); - lines2 = method2.findall (LINES_LIST); - merge (method1.find (LINES_ROOT), lines1, lines2, 'number', merge_lines); +def merge_lines(line1, line2): + """Merges two lines by summing their hits. Returns line1.""" -def merge_lines (line1, line2): - """Merges two lines by summing their hits. Returns line1.""" + # merge hits + value = int(line1.get('hits')) + int(line2.get('hits')) + line1.set('hits', str(value)) - # merge hits - value = int (line1.get('hits')) + int (line2.get('hits')) - line1.set ('hits', str(value)) + # merge conditionals + con1 = line1.get('condition-coverage') + con2 = line2.get('condition-coverage') + if con1 is not None and con2 is not None: + con1value = int(con1.split('%')[0]) + con2value = int(con2.split('%')[0]) + # bigger coverage on second line, swap their conditionals + if con2value > con1value: + line1.set('condition-coverage', str(con2)) + line1.__setitem__(0, line2.__getitem__(0)) - # merge conditionals - con1 = line1.get('condition-coverage') - con2 = line2.get('condition-coverage') - if (con1 is not None and con2 is not None): - con1value = int(con1.split('%')[0]) - con2value = int(con2.split('%')[0]) - # bigger coverage on second line, swap their conditionals - if (con2value > con1value): - line1.set ('condition-coverage', str(con2)) - line1.__setitem__(0, line2.__getitem__(0)) + return line1 - return line1 # prepare filters -prepare_packagefilters () +prepare_packagefilters() if filteronly: - # filter all given files - currfile = 1 - totalfiles = len (xmlfiles) - for xmlfile in xmlfiles: - xml = ET.parse(xmlfile) - filter_xml(xml) - logging.debug ('{1}/{2} filtering: {0}'.format (xmlfile, currfile, totalfiles)) - xml.write (xmlfile + filtersuffix, encoding="UTF-8", xml_declaration=True) - currfile += 1 + # filter all given files + currfile = 1 + totalfiles = len(xmlfiles) + for xmlfile in xmlfiles: + xml = ET.parse(xmlfile) + filter_xml(xml) + logging.debug('{1}/{2} filtering: {0}'.format(xmlfile, currfile, totalfiles)) + xml.write(xmlfile + filtersuffix, encoding="UTF-8", xml_declaration=True) + currfile += 1 else: - # merge all given files - totalfiles = len (xmlfiles) - - # special case if only one file was given - # filter given file and save it - if (totalfiles == 1): - logging.warning ('Only one file given!') - xmlfile = xmlfiles.pop(0) - xml = ET.parse(xmlfile) - filter_xml(xml) - xml.write (finalxml, encoding="UTF-8", xml_declaration=True) - sys.exit (0) - - - currfile = 1 - logging.debug ('{2}/{3} merging: {0} & {1}'.format (xmlfiles[0], xmlfiles[1], currfile, totalfiles-1)) - merge_xml (xmlfiles[0], xmlfiles[1], finalxml) - - - currfile = 2 - for i in range (totalfiles-2): - xmlfile = xmlfiles[i+2] - logging.debug ('{2}/{3} merging: {0} & {1}'.format (finalxml, xmlfile, currfile, totalfiles-1)) - merge_xml (finalxml, xmlfile, finalxml) - currfile += 1 + # merge all given files + totalfiles = len(xmlfiles) + + # special case if only one file was given + # filter given file and save it + if totalfiles == 1: + logging.warning('Only one file given!') + xmlfile = xmlfiles.pop(0) + xml = ET.parse(xmlfile) + filter_xml(xml) + xml.write(finalxml, encoding="UTF-8", xml_declaration=True) + sys.exit(0) + + currfile = 1 + logging.debug('{2}/{3} merging: {0} & {1}'.format(xmlfiles[0], xmlfiles[1], currfile, totalfiles - 1)) + merge_xml(xmlfiles[0], xmlfiles[1], finalxml) + + currfile = 2 + for i in range(totalfiles - 2): + xmlfile = xmlfiles[i + 2] + logging.debug('{2}/{3} merging: {0} & {1}'.format(finalxml, xmlfile, currfile, totalfiles - 1)) + merge_xml(finalxml, xmlfile, finalxml) + currfile += 1 diff --git a/setup.py b/setup.py index d87a45b1..50dbe9c2 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ class compile_translations(Command): try: os.environ.pop('DJANGO_SETTINGS_MODULE', None) from django.core.management import call_command + for dir in glob.glob('src/*'): for path, dirs, files in os.walk(dir): if 'locale' not in dirs: @@ -74,15 +75,18 @@ class install_lib(_install_lib): def get_version(): - '''Use the VERSION, if absent generates a version with git describe, if not - tag exists, take 0.0- and add the length of the commit log. - ''' + """Use the VERSION, if absent generates a version with git describe, if not + tag exists, take 0.0- and add the length of the commit log. + """ if os.path.exists('VERSION'): with open('VERSION', 'r') as v: return v.read() if os.path.exists('.git'): - p = subprocess.Popen(['git', 'describe', '--dirty=.dirty','--match=v*'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + ['git', 'describe', '--dirty=.dirty', '--match=v*'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) result = p.communicate()[0] if p.returncode == 0: result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v @@ -93,77 +97,77 @@ def get_version(): version = result return version else: - return '0.0.post%s' % len( - subprocess.check_output( - ['git', 'rev-list', 'HEAD']).splitlines()) + return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines()) return '0.0' -setup(name="authentic2", - version=get_version(), - license="AGPLv3+", - description="Authentic 2, a versatile identity management server", - url="http://dev.entrouvert.org/projects/authentic/", - author="Entr'ouvert", - author_email="authentic@listes.entrouvert.com", - maintainer="Benjamin Dauvergne", - maintainer_email="bdauvergne@entrouvert.com", - scripts=('authentic2-ctl',), - packages=find_packages('src'), - package_dir={ - '': 'src', - }, - include_package_data=True, - install_requires=[ - 'django>=1.11,<2.3', - 'requests>=2.3', - 'requests-oauthlib', - 'django-model-utils>=2.4,<4', - 'dnspython>=1.10', - 'Django-Select2>5,<6', - 'django-tables2>=1.0,<2.0', - 'django-ratelimit', - 'gadjo>=0.53', - 'django-import-export>=1,<2', - 'djangorestframework>=3.3,<3.10', - 'six>=1', - 'Markdown>=2.1', - 'python-ldap', - 'django-filter>1,<2.3', - 'pycryptodomex', - 'django-mellon>=1.22', - 'ldaptools', - 'jwcrypto>=0.3.1,<1', - 'cryptography', - 'XStatic-jQuery<2', - 'XStatic-jquery-ui', - 'xstatic-select2', - 'pillow', - 'tablib', - 'chardet', - 'attrs>17', - 'atomicwrites', - ], - zip_safe=False, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Django", - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Legal Industry', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Telecommunications Industry', - "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: System :: Systems Administration :: Authentication/Directory", - ], - cmdclass={ - 'build': build, - 'install_lib': install_lib, - 'compile_translations': compile_translations, - 'sdist': sdist, - }) +setup( + name="authentic2", + version=get_version(), + license="AGPLv3+", + description="Authentic 2, a versatile identity management server", + url="http://dev.entrouvert.org/projects/authentic/", + author="Entr'ouvert", + author_email="authentic@listes.entrouvert.com", + maintainer="Benjamin Dauvergne", + maintainer_email="bdauvergne@entrouvert.com", + scripts=('authentic2-ctl',), + packages=find_packages('src'), + package_dir={ + '': 'src', + }, + include_package_data=True, + install_requires=[ + 'django>=1.11,<2.3', + 'requests>=2.3', + 'requests-oauthlib', + 'django-model-utils>=2.4,<4', + 'dnspython>=1.10', + 'Django-Select2>5,<6', + 'django-tables2>=1.0,<2.0', + 'django-ratelimit', + 'gadjo>=0.53', + 'django-import-export>=1,<2', + 'djangorestframework>=3.3,<3.10', + 'six>=1', + 'Markdown>=2.1', + 'python-ldap', + 'django-filter>1,<2.3', + 'pycryptodomex', + 'django-mellon>=1.22', + 'ldaptools', + 'jwcrypto>=0.3.1,<1', + 'cryptography', + 'XStatic-jQuery<2', + 'XStatic-jquery-ui', + 'xstatic-select2', + 'pillow', + 'tablib', + 'chardet', + 'attrs>17', + 'atomicwrites', + ], + zip_safe=False, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Legal Industry', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Telecommunications Industry', + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: System :: Systems Administration :: Authentication/Directory", + ], + cmdclass={ + 'build': build, + 'install_lib': install_lib, + 'compile_translations': compile_translations, + 'sdist': sdist, + }, +) diff --git a/src/authentic2/a2_rbac/admin.py b/src/authentic2/a2_rbac/admin.py index b35745e2..1c5c46d8 100644 --- a/src/authentic2/a2_rbac/admin.py +++ b/src/authentic2/a2_rbac/admin.py @@ -27,8 +27,7 @@ class RoleParentInline(admin.TabularInline): fields = ['parent'] def get_queryset(self, request): - return super(RoleParentInline, self).get_queryset(request) \ - .filter(direct=True) + return super(RoleParentInline, self).get_queryset(request).filter(direct=True) class RoleChildInline(admin.TabularInline): @@ -37,8 +36,7 @@ class RoleChildInline(admin.TabularInline): fields = ['child'] def get_queryset(self, request): - return super(RoleChildInline, self).get_queryset(request) \ - .filter(direct=True) + return super(RoleChildInline, self).get_queryset(request).filter(direct=True) class RoleAttributeInline(admin.TabularInline): @@ -47,8 +45,18 @@ class RoleAttributeInline(admin.TabularInline): class RoleAdmin(admin.ModelAdmin): inlines = [RoleChildInline, RoleParentInline] - fields = ('uuid', 'name', 'slug', 'description', 'ou', 'members', - 'permissions', 'admin_scope_ct', 'admin_scope_id', 'service') + fields = ( + 'uuid', + 'name', + 'slug', + 'description', + 'ou', + 'members', + 'permissions', + 'admin_scope_ct', + 'admin_scope_id', + 'service', + ) readonly_fields = ('uuid',) prepopulated_fields = {"slug": ("name",)} filter_horizontal = ('members', 'permissions') @@ -59,9 +67,18 @@ class RoleAdmin(admin.ModelAdmin): class OrganizationalUnitAdmin(admin.ModelAdmin): - fields = ('uuid', 'name', 'slug', 'description', 'username_is_unique', - 'email_is_unique', 'default', 'validate_emails', - 'user_can_reset_password', 'user_add_password_policy') + fields = ( + 'uuid', + 'name', + 'slug', + 'description', + 'username_is_unique', + 'email_is_unique', + 'default', + 'validate_emails', + 'user_can_reset_password', + 'user_add_password_policy', + ) readonly_fields = ('uuid',) prepopulated_fields = {"slug": ("name",)} list_display = ('name', 'slug') @@ -74,8 +91,10 @@ class PermissionAdmin(admin.ModelAdmin): def name(self, obj): return six.text_type(obj) + name.short_description = _('name') + admin.site.register(models.Role, RoleAdmin) admin.site.register(models.OrganizationalUnit, OrganizationalUnitAdmin) admin.site.register(models.Permission, PermissionAdmin) diff --git a/src/authentic2/a2_rbac/app_settings.py b/src/authentic2/a2_rbac/app_settings.py index 35f0ccfc..515e9972 100644 --- a/src/authentic2/a2_rbac/app_settings.py +++ b/src/authentic2/a2_rbac/app_settings.py @@ -28,6 +28,7 @@ class AppSettings(object): def _setting(self, name, dflt): from django.conf import settings + return getattr(settings, name, dflt) def _setting_with_prefix(self, name, dflt): diff --git a/src/authentic2/a2_rbac/apps.py b/src/authentic2/a2_rbac/apps.py index 65cb2f00..737ea51f 100644 --- a/src/authentic2/a2_rbac/apps.py +++ b/src/authentic2/a2_rbac/apps.py @@ -27,26 +27,12 @@ class Authentic2RBACConfig(AppConfig): from authentic2.models import Service # update rbac on save to contenttype, ou and roles - post_save.connect( - signal_handlers.update_rbac_on_ou_post_save, - sender=models.OrganizationalUnit) - post_delete.connect( - signal_handlers.update_rbac_on_ou_post_delete, - sender=models.OrganizationalUnit) + post_save.connect(signal_handlers.update_rbac_on_ou_post_save, sender=models.OrganizationalUnit) + post_delete.connect(signal_handlers.update_rbac_on_ou_post_delete, sender=models.OrganizationalUnit) # keep service role and service ou field in sync for subclass in Service.__subclasses__(): - post_save.connect( - signal_handlers.update_service_role_ou, - sender=subclass) - post_save.connect( - signal_handlers.update_service_role_ou, - sender=Service) - post_migrate.connect( - signal_handlers.create_default_ou, - sender=self) - post_migrate.connect( - signal_handlers.create_default_permissions, - sender=self) - post_migrate.connect( - signal_handlers.post_migrate_update_rbac, - sender=self) + post_save.connect(signal_handlers.update_service_role_ou, sender=subclass) + post_save.connect(signal_handlers.update_service_role_ou, sender=Service) + post_migrate.connect(signal_handlers.create_default_ou, sender=self) + post_migrate.connect(signal_handlers.create_default_permissions, sender=self) + post_migrate.connect(signal_handlers.post_migrate_update_rbac, sender=self) diff --git a/src/authentic2/a2_rbac/fields.py b/src/authentic2/a2_rbac/fields.py index c269d3bf..4b447f9b 100644 --- a/src/authentic2/a2_rbac/fields.py +++ b/src/authentic2/a2_rbac/fields.py @@ -19,9 +19,10 @@ from django import forms class UniqueBooleanField(NullBooleanField): - '''BooleanField allowing only one True value in the table, and preventing - problems with multiple False values by implicitely converting them to - None.''' + """BooleanField allowing only one True value in the table, and preventing + problems with multiple False values by implicitely converting them to + None.""" + def __init__(self, *args, **kwargs): kwargs['unique'] = True kwargs['blank'] = True @@ -44,8 +45,7 @@ class UniqueBooleanField(NullBooleanField): return value def get_db_prep_value(self, value, connection, prepared=False): - value = super(UniqueBooleanField, self).get_db_prep_value( - value, connection, prepared=prepared) + value = super(UniqueBooleanField, self).get_db_prep_value(value, connection, prepared=prepared) if value is False: return None return value diff --git a/src/authentic2/a2_rbac/management.py b/src/authentic2/a2_rbac/management.py index 129e408d..030d701f 100644 --- a/src/authentic2/a2_rbac/management.py +++ b/src/authentic2/a2_rbac/management.py @@ -33,8 +33,7 @@ def update_ou_admin_roles(ou): Role = get_role_model() if app_settings.MANAGED_CONTENT_TYPES == (): - Role.objects.filter(slug='a2-managers-of-{ou.slug}'.format(ou=ou)) \ - .delete() + Role.objects.filter(slug='a2-managers-of-{ou.slug}'.format(ou=ou)).delete() else: ou_admin_role = ou.get_admin_role() @@ -55,14 +54,9 @@ def update_ou_admin_roles(ou): continue else: ou_ct_admin_role = Role.objects.get_admin_role( - instance=ct, - ou=ou, - name=name, - slug=ou_slug, - update_slug=True, - update_name=True) - if not app_settings.MANAGED_CONTENT_TYPES or \ - key in app_settings.MANAGED_CONTENT_TYPES: + instance=ct, ou=ou, name=name, slug=ou_slug, update_slug=True, update_name=True + ) + if not app_settings.MANAGED_CONTENT_TYPES or key in app_settings.MANAGED_CONTENT_TYPES: ou_ct_admin_role.add_child(ou_admin_role) else: ou_ct_admin_role.remove_child(ou_admin_role) @@ -72,10 +66,10 @@ def update_ou_admin_roles(ou): def update_ous_admin_roles(): - '''Create general admin roles linked to all organizational units, - they give general administrative rights to all mamanged content types - scoped to the given organizational unit. - ''' + """Create general admin roles linked to all organizational units, + they give general administrative rights to all mamanged content types + scoped to the given organizational unit. + """ OU = get_ou_model() ou_all = OU.objects.all() if len(ou_all) < 2: @@ -85,6 +79,7 @@ def update_ous_admin_roles(): for ou in ou_all: update_ou_admin_roles(ou) + MANAGED_CT = { ('a2_rbac', 'role'): { 'name': _('Manager of roles'), @@ -108,9 +103,9 @@ MANAGED_CT = { def update_content_types_roles(): - '''Create general and scoped management roles for all managed content - types. - ''' + """Create general and scoped management roles for all managed content + types. + """ cts = ContentType.objects.all() Role = get_role_model() view_user_perm = utils.get_view_user_perm() @@ -120,10 +115,7 @@ def update_content_types_roles(): if app_settings.MANAGED_CONTENT_TYPES == (): Role.objects.filter(slug=slug).delete() else: - admin_role, created = Role.objects.get_or_create( - slug=slug, - defaults=dict( - name=ugettext('Manager'))) + admin_role, created = Role.objects.get_or_create(slug=slug, defaults=dict(name=ugettext('Manager'))) admin_role.add_self_administration() if not created and admin_role.name != ugettext('Manager'): admin_role.name = ugettext('Manager') @@ -136,15 +128,15 @@ def update_content_types_roles(): # General admin role name = six.text_type(MANAGED_CT[ct_tuple]['name']) slug = '_a2-' + slugify(name) - if app_settings.MANAGED_CONTENT_TYPES is not None and ct_tuple not in \ - app_settings.MANAGED_CONTENT_TYPES: + if ( + app_settings.MANAGED_CONTENT_TYPES is not None + and ct_tuple not in app_settings.MANAGED_CONTENT_TYPES + ): Role.objects.filter(slug=slug).delete() continue - ct_admin_role = Role.objects.get_admin_role(instance=ct, name=name, - slug=slug, - update_name=True, - update_slug=True, - create=True) + ct_admin_role = Role.objects.get_admin_role( + instance=ct, name=name, slug=slug, update_name=True, update_slug=True, create=True + ) if MANAGED_CT[ct_tuple].get('must_view_user'): ct_admin_role.permissions.add(view_user_perm) if MANAGED_CT[ct_tuple].get('must_manage_authorizations_user'): diff --git a/src/authentic2/a2_rbac/managers.py b/src/authentic2/a2_rbac/managers.py index 6aea627a..98066503 100644 --- a/src/authentic2/a2_rbac/managers.py +++ b/src/authentic2/a2_rbac/managers.py @@ -28,13 +28,27 @@ class OrganizationalUnitManager(AbstractBaseManager): class RoleManager(BaseRoleManager): - def get_admin_role(self, instance, name, slug, ou=None, operation=ADMIN_OP, - update_name=False, update_slug=False, permissions=(), - self_administered=False, create=True): + def get_admin_role( + self, + instance, + name, + slug, + ou=None, + operation=ADMIN_OP, + update_name=False, + update_slug=False, + permissions=(), + self_administered=False, + create=True, + ): '''Get or create the role of manager's of this object instance''' kwargs = {} - assert not ou or isinstance(instance, ContentType), ( - 'get_admin_role(ou=...) can only be used with ContentType instances: %s %s %s' % (name, ou, instance) + assert not ou or isinstance( + instance, ContentType + ), 'get_admin_role(ou=...) can only be used with ContentType instances: %s %s %s' % ( + name, + ou, + instance, ) # Does the permission need to be scoped by ou ? Yes if the target is a @@ -57,14 +71,16 @@ class RoleManager(BaseRoleManager): target_ct=ContentType.objects.get_for_model(instance), target_id=instance.pk, defaults=defaults, - **kwargs) + **kwargs, + ) else: try: perm = Permission.objects.get( operation=op, target_ct=ContentType.objects.get_for_model(instance), target_id=instance.pk, - **kwargs) + **kwargs, + ) except Permission.DoesNotExist: return None @@ -75,10 +91,15 @@ class RoleManager(BaseRoleManager): mirror_role_ou = instance.ou else: mirror_role_ou = None - admin_role = self.get_mirror_role(perm, name, slug, ou=mirror_role_ou, - update_name=update_name, - update_slug=update_slug, - create=create) + admin_role = self.get_mirror_role( + perm, + name, + slug, + ou=mirror_role_ou, + update_name=update_name, + update_slug=update_slug, + create=create, + ) if not admin_role: return None @@ -92,11 +113,12 @@ class RoleManager(BaseRoleManager): admin_role.permissions.set(permissions) return admin_role - def get_mirror_role(self, instance, name, slug, ou=None, - update_name=False, update_slug=False, create=True): - '''Get or create a role which mirrors another model, for example a - permission. - ''' + def get_mirror_role( + self, instance, name, slug, ou=None, update_name=False, update_slug=False, create=True + ): + """Get or create a role which mirrors another model, for example a + permission. + """ ct = ContentType.objects.get_for_model(instance) update_fields = {} kwargs = {} @@ -111,16 +133,13 @@ class RoleManager(BaseRoleManager): if create: role, _ = self.prefetch_related('permissions').update_or_create( - admin_scope_ct=ct, - admin_scope_id=instance.pk, - defaults=update_fields, - **kwargs) + admin_scope_ct=ct, admin_scope_id=instance.pk, defaults=update_fields, **kwargs + ) else: try: role = self.prefetch_related('permissions').get( - admin_scope_ct=ct, - admin_scope_id=instance.pk, - **kwargs) + admin_scope_ct=ct, admin_scope_id=instance.pk, **kwargs + ) except self.model.DoesNotExist: return None for field, value in update_fields.items(): diff --git a/src/authentic2/a2_rbac/migrations/0001_initial.py b/src/authentic2/a2_rbac/migrations/0001_initial.py index 82d5808b..c6106014 100644 --- a/src/authentic2/a2_rbac/migrations/0001_initial.py +++ b/src/authentic2/a2_rbac/migrations/0001_initial.py @@ -20,12 +20,23 @@ class Migration(migrations.Migration): migrations.CreateModel( name='OrganizationalUnit', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('uuid', models.CharField(default=authentic2.utils.get_hex_uuid, unique=True, max_length=32, verbose_name='uuid')), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), + ( + 'uuid', + models.CharField( + default=authentic2.utils.get_hex_uuid, unique=True, max_length=32, verbose_name='uuid' + ), + ), ('name', models.CharField(max_length=256, verbose_name='name')), ('slug', models.SlugField(max_length=256, verbose_name='slug')), ('description', models.TextField(verbose_name='description', blank=True)), - ('default', authentic2.a2_rbac.fields.UniqueBooleanField(verbose_name='Default organizational unit')), + ( + 'default', + authentic2.a2_rbac.fields.UniqueBooleanField(verbose_name='Default organizational unit'), + ), ], options={ 'verbose_name': 'organizational unit', @@ -36,11 +47,33 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Permission', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('target_id', models.PositiveIntegerField()), - ('operation', models.ForeignKey(verbose_name='operation', to='django_rbac.Operation', on_delete=models.CASCADE)), - ('ou', models.ForeignKey(related_name='scoped_permission', verbose_name='organizational unit', to=settings.RBAC_OU_MODEL, null=True, on_delete=models.CASCADE)), - ('target_ct', models.ForeignKey(related_name='+', to='contenttypes.ContentType', on_delete=models.CASCADE)), + ( + 'operation', + models.ForeignKey( + verbose_name='operation', to='django_rbac.Operation', on_delete=models.CASCADE + ), + ), + ( + 'ou', + models.ForeignKey( + related_name='scoped_permission', + verbose_name='organizational unit', + to=settings.RBAC_OU_MODEL, + null=True, + on_delete=models.CASCADE, + ), + ), + ( + 'target_ct', + models.ForeignKey( + related_name='+', to='contenttypes.ContentType', on_delete=models.CASCADE + ), + ), ], options={ 'verbose_name': 'permission', @@ -51,17 +84,65 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Role', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('uuid', models.CharField(default=authentic2.utils.get_hex_uuid, unique=True, max_length=32, verbose_name='uuid')), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), + ( + 'uuid', + models.CharField( + default=authentic2.utils.get_hex_uuid, unique=True, max_length=32, verbose_name='uuid' + ), + ), ('name', models.CharField(max_length=256, verbose_name='name')), ('slug', models.SlugField(max_length=256, verbose_name='slug')), ('description', models.TextField(verbose_name='description', blank=True)), - ('admin_scope_id', models.PositiveIntegerField(null=True, verbose_name='administrative scope id', blank=True)), - ('admin_scope_ct', models.ForeignKey(verbose_name='administrative scope content type', blank=True, to='contenttypes.ContentType', null=True, on_delete=models.CASCADE)), - ('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL, blank=True)), - ('ou', models.ForeignKey(verbose_name='organizational unit', blank=True, to=settings.RBAC_OU_MODEL, null=True, on_delete=models.CASCADE)), - ('permissions', models.ManyToManyField(related_name='role', to=settings.RBAC_PERMISSION_MODEL, blank=True)), - ('service', models.ForeignKey(verbose_name='service', blank=True, to='authentic2.Service', null=True, on_delete=models.CASCADE)), + ( + 'admin_scope_id', + models.PositiveIntegerField( + null=True, verbose_name='administrative scope id', blank=True + ), + ), + ( + 'admin_scope_ct', + models.ForeignKey( + verbose_name='administrative scope content type', + blank=True, + to='contenttypes.ContentType', + null=True, + on_delete=models.CASCADE, + ), + ), + ( + 'members', + models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL, blank=True), + ), + ( + 'ou', + models.ForeignKey( + verbose_name='organizational unit', + blank=True, + to=settings.RBAC_OU_MODEL, + null=True, + on_delete=models.CASCADE, + ), + ), + ( + 'permissions', + models.ManyToManyField( + related_name='role', to=settings.RBAC_PERMISSION_MODEL, blank=True + ), + ), + ( + 'service', + models.ForeignKey( + verbose_name='service', + blank=True, + to='authentic2.Service', + null=True, + on_delete=models.CASCADE, + ), + ), ], options={ 'ordering': ('ou', 'service', 'name'), @@ -73,11 +154,25 @@ class Migration(migrations.Migration): migrations.CreateModel( name='RoleAttribute', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(max_length=64, verbose_name='name')), - ('kind', models.CharField(max_length=32, verbose_name='kind', choices=[('string', 'string')])), + ( + 'kind', + models.CharField(max_length=32, verbose_name='kind', choices=[('string', 'string')]), + ), ('value', models.TextField(verbose_name='value')), - ('role', models.ForeignKey(related_name='attributes', verbose_name='role', to=settings.RBAC_ROLE_MODEL, on_delete=models.CASCADE)), + ( + 'role', + models.ForeignKey( + related_name='attributes', + verbose_name='role', + to=settings.RBAC_ROLE_MODEL, + on_delete=models.CASCADE, + ), + ), ], options={ 'verbose_name': 'role attribute', @@ -88,10 +183,23 @@ class Migration(migrations.Migration): migrations.CreateModel( name='RoleParenting', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('direct', models.BooleanField(blank=True, default=True)), - ('child', models.ForeignKey(related_name='parent_relation', to=settings.RBAC_ROLE_MODEL, on_delete=models.CASCADE)), - ('parent', models.ForeignKey(related_name='child_relation', to=settings.RBAC_ROLE_MODEL, on_delete=models.CASCADE)), + ( + 'child', + models.ForeignKey( + related_name='parent_relation', to=settings.RBAC_ROLE_MODEL, on_delete=models.CASCADE + ), + ), + ( + 'parent', + models.ForeignKey( + related_name='child_relation', to=settings.RBAC_ROLE_MODEL, on_delete=models.CASCADE + ), + ), ], options={ 'verbose_name': 'role parenting relation', diff --git a/src/authentic2/a2_rbac/migrations/0003_partial_unique_index_on_name_and_slug.py b/src/authentic2/a2_rbac/migrations/0003_partial_unique_index_on_name_and_slug.py index 4e3d029f..1283a296 100644 --- a/src/authentic2/a2_rbac/migrations/0003_partial_unique_index_on_name_and_slug.py +++ b/src/authentic2/a2_rbac/migrations/0003_partial_unique_index_on_name_and_slug.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.db import models, migrations from authentic2.migrations import CreatePartialIndexes + class Migration(migrations.Migration): dependencies = [ @@ -11,7 +12,12 @@ class Migration(migrations.Migration): ] operations = [ - CreatePartialIndexes('Role', 'a2_rbac_role', 'a2_rbac_role_unique_idx', - ('ou_id', 'service_id'), ('slug',), - null_columns=('admin_scope_ct_id',)), + CreatePartialIndexes( + 'Role', + 'a2_rbac_role', + 'a2_rbac_role_unique_idx', + ('ou_id', 'service_id'), + ('slug',), + null_columns=('admin_scope_ct_id',), + ), ] diff --git a/src/authentic2/a2_rbac/migrations/0004_auto_20150523_0028.py b/src/authentic2/a2_rbac/migrations/0004_auto_20150523_0028.py index 69f88a16..d6545dc8 100644 --- a/src/authentic2/a2_rbac/migrations/0004_auto_20150523_0028.py +++ b/src/authentic2/a2_rbac/migrations/0004_auto_20150523_0028.py @@ -13,7 +13,11 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='organizationalunit', - options={'ordering': ('name',), 'verbose_name': 'organizational unit', 'verbose_name_plural': 'organizational units'}, + options={ + 'ordering': ('name',), + 'verbose_name': 'organizational unit', + 'verbose_name_plural': 'organizational units', + }, ), migrations.AlterUniqueTogether( name='role', diff --git a/src/authentic2/a2_rbac/migrations/0005_auto_20150526_1406.py b/src/authentic2/a2_rbac/migrations/0005_auto_20150526_1406.py index 9a8ccef0..b64ac084 100644 --- a/src/authentic2/a2_rbac/migrations/0005_auto_20150526_1406.py +++ b/src/authentic2/a2_rbac/migrations/0005_auto_20150526_1406.py @@ -14,7 +14,14 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='role', name='service', - field=models.ForeignKey(related_name='roles', verbose_name='service', blank=True, to='authentic2.Service', null=True, on_delete=models.CASCADE), + field=models.ForeignKey( + related_name='roles', + verbose_name='service', + blank=True, + to='authentic2.Service', + null=True, + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/src/authentic2/a2_rbac/migrations/0008_auto_20150810_1953.py b/src/authentic2/a2_rbac/migrations/0008_auto_20150810_1953.py index 83d466d7..94507113 100644 --- a/src/authentic2/a2_rbac/migrations/0008_auto_20150810_1953.py +++ b/src/authentic2/a2_rbac/migrations/0008_auto_20150810_1953.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='organizationalunit', - options={'ordering': ('default', 'name'), 'verbose_name': 'organizational unit', 'verbose_name_plural': 'organizational units'}, + options={ + 'ordering': ('default', 'name'), + 'verbose_name': 'organizational unit', + 'verbose_name_plural': 'organizational units', + }, ), ] diff --git a/src/authentic2/a2_rbac/migrations/0009_partial_unique_index_on_permission.py b/src/authentic2/a2_rbac/migrations/0009_partial_unique_index_on_permission.py index 86365b19..ce496741 100644 --- a/src/authentic2/a2_rbac/migrations/0009_partial_unique_index_on_permission.py +++ b/src/authentic2/a2_rbac/migrations/0009_partial_unique_index_on_permission.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.db import models, migrations from authentic2.migrations import CreatePartialIndexes + class Migration(migrations.Migration): dependencies = [ @@ -11,6 +12,11 @@ class Migration(migrations.Migration): ] operations = [ - CreatePartialIndexes('Permission', 'a2_rbac_permission', 'a2_rbac_permission_null_ou_unique_idx', - ('ou_id',), ('operation_id', 'target_ct_id', 'target_id')) + CreatePartialIndexes( + 'Permission', + 'a2_rbac_permission', + 'a2_rbac_permission_null_ou_unique_idx', + ('ou_id',), + ('operation_id', 'target_ct_id', 'target_id'), + ) ] diff --git a/src/authentic2/a2_rbac/migrations/0010_auto_20160209_1417.py b/src/authentic2/a2_rbac/migrations/0010_auto_20160209_1417.py index 73859577..1a0575b6 100644 --- a/src/authentic2/a2_rbac/migrations/0010_auto_20160209_1417.py +++ b/src/authentic2/a2_rbac/migrations/0010_auto_20160209_1417.py @@ -6,14 +6,13 @@ from django.db import migrations def deduplicate_admin_roles(apps, schema_editor): - '''Find duplicated admin roles, only keep the one with the lowest id and - copy all members, parent and children of other duplicated roles to it, - then delete duplicates with greater id. - ''' + """Find duplicated admin roles, only keep the one with the lowest id and + copy all members, parent and children of other duplicated roles to it, + then delete duplicates with greater id. + """ Role = apps.get_model('a2_rbac', 'Role') RoleParenting = apps.get_model('a2_rbac', 'RoleParenting') - qs = Role.objects.filter(admin_scope_ct__isnull=False, - admin_scope_id__isnull=False).order_by('id') + qs = Role.objects.filter(admin_scope_ct__isnull=False, admin_scope_id__isnull=False).order_by('id') roles = defaultdict(lambda: []) for role in qs: @@ -26,21 +25,13 @@ def deduplicate_admin_roles(apps, schema_editor): children = set() for role in duplicates: members |= set(role.members.all()) - parents |= set( - rp.parent for rp in RoleParenting.objects.filter(child=role, direct=True)) - children |= set( - rp.child for rp in RoleParenting.objects.filter(parent=role, direct=True)) + parents |= set(rp.parent for rp in RoleParenting.objects.filter(child=role, direct=True)) + children |= set(rp.child for rp in RoleParenting.objects.filter(parent=role, direct=True)) duplicates[0].members = members for parent in parents: - RoleParenting.objects.get_or_crate( - parent=parent, - child=duplicates[0], - direct=True) + RoleParenting.objects.get_or_crate(parent=parent, child=duplicates[0], direct=True) for child in children: - RoleParenting.objects.get_or_create( - parent=duplicates[0], - child=child, - direct=True) + RoleParenting.objects.get_or_create(parent=duplicates[0], child=child, direct=True) for role in duplicates[1:]: role.delete() diff --git a/src/authentic2/a2_rbac/migrations/0014_auto_20170711_1024.py b/src/authentic2/a2_rbac/migrations/0014_auto_20170711_1024.py index 856099b6..aeb6a85a 100644 --- a/src/authentic2/a2_rbac/migrations/0014_auto_20170711_1024.py +++ b/src/authentic2/a2_rbac/migrations/0014_auto_20170711_1024.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='organizationalunit', - options={'ordering': ('-default', 'name'), 'verbose_name': 'organizational unit', 'verbose_name_plural': 'organizational units'}, + options={ + 'ordering': ('-default', 'name'), + 'verbose_name': 'organizational unit', + 'verbose_name_plural': 'organizational units', + }, ), ] diff --git a/src/authentic2/a2_rbac/migrations/0016_auto_20171208_1429.py b/src/authentic2/a2_rbac/migrations/0016_auto_20171208_1429.py index bbe52dfc..e8c4d7fa 100644 --- a/src/authentic2/a2_rbac/migrations/0016_auto_20171208_1429.py +++ b/src/authentic2/a2_rbac/migrations/0016_auto_20171208_1429.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='organizationalunit', - options={'ordering': ('name',), 'verbose_name': 'organizational unit', 'verbose_name_plural': 'organizational units'}, + options={ + 'ordering': ('name',), + 'verbose_name': 'organizational unit', + 'verbose_name_plural': 'organizational units', + }, ), ] diff --git a/src/authentic2/a2_rbac/migrations/0018_organizationalunit_user_add_password_policy.py b/src/authentic2/a2_rbac/migrations/0018_organizationalunit_user_add_password_policy.py index 72285beb..ac5ea45e 100644 --- a/src/authentic2/a2_rbac/migrations/0018_organizationalunit_user_add_password_policy.py +++ b/src/authentic2/a2_rbac/migrations/0018_organizationalunit_user_add_password_policy.py @@ -14,6 +14,10 @@ class Migration(migrations.Migration): migrations.AddField( model_name='organizationalunit', name='user_add_password_policy', - field=models.IntegerField(default=0, verbose_name='User creation password policy', choices=[(0, 'Send reset link'), (1, 'Manual password definition')]), + field=models.IntegerField( + default=0, + verbose_name='User creation password policy', + choices=[(0, 'Send reset link'), (1, 'Manual password definition')], + ), ), ] diff --git a/src/authentic2/a2_rbac/migrations/0020_partial_unique_index_on_name.py b/src/authentic2/a2_rbac/migrations/0020_partial_unique_index_on_name.py index 00e8d72c..039b710c 100644 --- a/src/authentic2/a2_rbac/migrations/0020_partial_unique_index_on_name.py +++ b/src/authentic2/a2_rbac/migrations/0020_partial_unique_index_on_name.py @@ -12,7 +12,12 @@ class Migration(migrations.Migration): ] operations = [ - CreatePartialIndexes('Role', 'a2_rbac_role', 'a2_rbac_role_name_unique_idx', - ('ou_id',), ('name',), - null_columns=('admin_scope_ct_id',)), + CreatePartialIndexes( + 'Role', + 'a2_rbac_role', + 'a2_rbac_role_name_unique_idx', + ('ou_id',), + ('name',), + null_columns=('admin_scope_ct_id',), + ), ] diff --git a/src/authentic2/a2_rbac/migrations/0021_auto_20200317_1514.py b/src/authentic2/a2_rbac/migrations/0021_auto_20200317_1514.py index 759161f8..b08f6d5b 100644 --- a/src/authentic2/a2_rbac/migrations/0021_auto_20200317_1514.py +++ b/src/authentic2/a2_rbac/migrations/0021_auto_20200317_1514.py @@ -16,11 +16,15 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='organizationalunit', name='uuid', - field=models.CharField(default=django_rbac.utils.get_hex_uuid, max_length=32, unique=True, verbose_name='uuid'), + field=models.CharField( + default=django_rbac.utils.get_hex_uuid, max_length=32, unique=True, verbose_name='uuid' + ), ), migrations.AlterField( model_name='role', name='uuid', - field=models.CharField(default=django_rbac.utils.get_hex_uuid, max_length=32, unique=True, verbose_name='uuid'), + field=models.CharField( + default=django_rbac.utils.get_hex_uuid, max_length=32, unique=True, verbose_name='uuid' + ), ), ] diff --git a/src/authentic2/a2_rbac/migrations/0022_auto_20200402_1101.py b/src/authentic2/a2_rbac/migrations/0022_auto_20200402_1101.py index 7eb237b9..6b24a846 100644 --- a/src/authentic2/a2_rbac/migrations/0022_auto_20200402_1101.py +++ b/src/authentic2/a2_rbac/migrations/0022_auto_20200402_1101.py @@ -16,11 +16,29 @@ class Migration(migrations.Migration): migrations.AddField( model_name='organizationalunit', name='clean_unused_accounts_alert', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(30, 'Ensure that this value is greater than 30 days, or leave blank for deactivating.')], verbose_name='Days after which the user receives an account deletion alert'), + field=models.PositiveIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator( + 30, 'Ensure that this value is greater than 30 days, or leave blank for deactivating.' + ) + ], + verbose_name='Days after which the user receives an account deletion alert', + ), ), migrations.AddField( model_name='organizationalunit', name='clean_unused_accounts_deletion', - field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(30, 'Ensure that this value is greater than 30 days, or leave blank for deactivating.')], verbose_name='Delay in days before cleaning unused accounts'), + field=models.PositiveIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator( + 30, 'Ensure that this value is greater than 30 days, or leave blank for deactivating.' + ) + ], + verbose_name='Delay in days before cleaning unused accounts', + ), ), ] diff --git a/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py b/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py index 16261beb..05ff20c7 100644 --- a/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py +++ b/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py @@ -39,6 +39,4 @@ class Migration(migrations.Migration): ('a2_rbac', '0023_role_can_manage_members'), ] - operations = [ - migrations.RunPython(update_self_administration_perm, migrations.RunPython.noop) - ] + operations = [migrations.RunPython(update_self_administration_perm, migrations.RunPython.noop)] diff --git a/src/authentic2/a2_rbac/models.py b/src/authentic2/a2_rbac/models.py index 9d7578b6..8c796e2d 100644 --- a/src/authentic2/a2_rbac/models.py +++ b/src/authentic2/a2_rbac/models.py @@ -23,20 +23,23 @@ from django.utils.text import slugify from django.db import models from django.contrib.contenttypes.models import ContentType -from django_rbac.models import (RoleAbstractBase, PermissionAbstractBase, - OrganizationalUnitAbstractBase, RoleParentingAbstractBase, VIEW_OP, - Operation) +from django_rbac.models import ( + RoleAbstractBase, + PermissionAbstractBase, + OrganizationalUnitAbstractBase, + RoleParentingAbstractBase, + VIEW_OP, + Operation, +) from django_rbac import utils as rbac_utils from authentic2.decorators import errorcollector try: - from django.contrib.contenttypes.fields import GenericForeignKey, \ - GenericRelation + from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation except ImportError: # Django < 1.8 - from django.contrib.contenttypes.generic import GenericForeignKey, \ - GenericRelation + from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation from authentic2.decorators import GlobalCache @@ -53,63 +56,55 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase): (MANUAL_PASSWORD_POLICY, _('Manual password definition')), ) - PolicyValue = namedtuple('PolicyValue', [ - 'generate_password', 'reset_password_at_next_login', - 'send_mail', 'send_password_reset']) + PolicyValue = namedtuple( + 'PolicyValue', + ['generate_password', 'reset_password_at_next_login', 'send_mail', 'send_password_reset'], + ) USER_ADD_PASSWD_POLICY_VALUES = { RESET_LINK_POLICY: PolicyValue(False, False, False, True), MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False), } - username_is_unique = models.BooleanField( - blank=True, - default=False, - verbose_name=_('Username is unique')) - email_is_unique = models.BooleanField( - blank=True, - default=False, - verbose_name=_('Email is unique')) - default = fields.UniqueBooleanField( - verbose_name=_('Default organizational unit')) + username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique')) + email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique')) + default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit')) - validate_emails = models.BooleanField( - blank=True, - default=False, - verbose_name=_('Validate emails')) + validate_emails = models.BooleanField(blank=True, default=False, verbose_name=_('Validate emails')) - show_username = models.BooleanField( - blank=True, - default=True, - verbose_name=_('Show username')) + show_username = models.BooleanField(blank=True, default=True, verbose_name=_('Show username')) - admin_perms = GenericRelation(rbac_utils.get_permission_model_name(), - content_type_field='target_ct', - object_id_field='target_id') + admin_perms = GenericRelation( + rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id' + ) - user_can_reset_password = models.NullBooleanField( - verbose_name=_('Users can reset password')) + user_can_reset_password = models.NullBooleanField(verbose_name=_('Users can reset password')) user_add_password_policy = models.IntegerField( - verbose_name=_('User creation password policy'), - choices=USER_ADD_PASSWD_POLICY_CHOICES, - default=0) + verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0 + ) clean_unused_accounts_alert = models.PositiveIntegerField( verbose_name=_('Days after which the user receives an account deletion alert'), - validators=[MinValueValidator( - 30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.') - )], + validators=[ + MinValueValidator( + 30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.') + ) + ], null=True, - blank=True) + blank=True, + ) clean_unused_accounts_deletion = models.PositiveIntegerField( verbose_name=_('Delay in days before cleaning unused accounts'), - validators=[MinValueValidator( - 30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.') - )], + validators=[ + MinValueValidator( + 30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.') + ) + ], null=True, - blank=True) + blank=True, + ) objects = managers.OrganizationalUnitManager() @@ -130,27 +125,38 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase): if self.pk: qs = qs.exclude(pk=self.pk) qs.update(default=None) - if self.pk and not self.default \ - and self.__class__.objects.get(pk=self.pk).default: - raise ValidationError(_('You cannot unset this organizational ' - 'unit as the default, but you can set ' - 'another one as the default.')) + if self.pk and not self.default and self.__class__.objects.get(pk=self.pk).default: + raise ValidationError( + _( + 'You cannot unset this organizational ' + 'unit as the default, but you can set ' + 'another one as the default.' + ) + ) if bool(self.clean_unused_accounts_alert) ^ bool(self.clean_unused_accounts_deletion): raise ValidationError(_('Deletion and alert delays must be set together.')) - if self.clean_unused_accounts_alert and \ - self.clean_unused_accounts_alert >= self.clean_unused_accounts_deletion: + if ( + self.clean_unused_accounts_alert + and self.clean_unused_accounts_alert >= self.clean_unused_accounts_deletion + ): raise ValidationError(_('Deletion alert delay must be less than actual deletion delay.')) super(OrganizationalUnit, self).clean() def get_admin_role(self): - '''Get or create the generic admin role for this organizational - unit. - ''' + """Get or create the generic admin role for this organizational + unit. + """ name = _('Managers of "{ou}"').format(ou=self) slug = '_a2-managers-of-{ou.slug}'.format(ou=self) return Role.objects.get_admin_role( - instance=self, name=name, slug=slug, operation=VIEW_OP, - update_name=True, update_slug=True, create=True) + instance=self, + name=name, + slug=slug, + operation=VIEW_OP, + update_name=True, + update_slug=True, + create=True, + ) def delete(self, *args, **kwargs): Permission.objects.filter(ou=self).delete() @@ -166,11 +172,14 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase): def export_json(self): return { - 'uuid': self.uuid, 'slug': self.slug, 'name': self.name, - 'description': self.description, 'default': self.default, + 'uuid': self.uuid, + 'slug': self.slug, + 'name': self.name, + 'description': self.description, + 'default': self.default, 'email_is_unique': self.email_is_unique, 'username_is_unique': self.username_is_unique, - 'validate_emails': self.validate_emails + 'validate_emails': self.validate_emails, } def __str__(self): @@ -185,9 +194,11 @@ class Permission(PermissionAbstractBase): verbose_name = _('permission') verbose_name_plural = _('permissions') - mirror_roles = GenericRelation(rbac_utils.get_role_model_name(), - content_type_field='admin_scope_ct', - object_id_field='admin_scope_id') + mirror_roles = GenericRelation( + rbac_utils.get_role_model_name(), + content_type_field='admin_scope_ct', + object_id_field='admin_scope_id', + ) Permission._meta.natural_key = [ @@ -202,33 +213,29 @@ class Role(RoleAbstractBase): null=True, blank=True, verbose_name=_('administrative scope content type'), - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) admin_scope_id = models.PositiveIntegerField( - verbose_name=_('administrative scope id'), - null=True, - blank=True) - admin_scope = GenericForeignKey( - 'admin_scope_ct', - 'admin_scope_id') + verbose_name=_('administrative scope id'), null=True, blank=True + ) + admin_scope = GenericForeignKey('admin_scope_ct', 'admin_scope_id') service = models.ForeignKey( to='authentic2.Service', verbose_name=_('service'), null=True, blank=True, related_name='roles', - on_delete=models.CASCADE) - external_id = models.TextField( - verbose_name=_('external id'), - blank=True, - db_index=True) + on_delete=models.CASCADE, + ) + external_id = models.TextField(verbose_name=_('external id'), blank=True, db_index=True) - admin_perms = GenericRelation(rbac_utils.get_permission_model_name(), - content_type_field='target_ct', - object_id_field='target_id') + admin_perms = GenericRelation( + rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id' + ) can_manage_members = models.BooleanField( - default=True, - verbose_name=_('Allow adding or deleting role members')) + default=True, verbose_name=_('Allow adding or deleting role members') + ) def get_admin_role(self, create=True): from . import utils @@ -240,16 +247,15 @@ class Role(RoleAbstractBase): admin_role = self.__class__.objects.get_admin_role( self, - name=_('Managers of role "{role}"').format( - role=six.text_type(self)), - slug='_a2-managers-of-role-{role}'.format( - role=slugify(six.text_type(self))), + name=_('Managers of role "{role}"').format(role=six.text_type(self)), + slug='_a2-managers-of-role-{role}'.format(role=slugify(six.text_type(self))), permissions=(view_user_perm,), self_administered=True, update_name=True, update_slug=True, create=create, - operation=MANAGE_MEMBERS_OP) + operation=MANAGE_MEMBERS_OP, + ) return admin_role def validate_unique(self, exclude=None): @@ -294,7 +300,8 @@ class Role(RoleAbstractBase): operation=operation, target_ct=ContentType.objects.get_for_model(self), target_id=self.pk, - ou__is_null=True) + ou__is_null=True, + ) return self.permissions.filter(pk=self_perm.pk).exists() def add_self_administration(self, op=None): @@ -304,9 +311,8 @@ class Role(RoleAbstractBase): Permission = rbac_utils.get_permission_model() operation = rbac_utils.get_operation(op) self_perm, created = Permission.objects.get_or_create( - operation=operation, - target_ct=ContentType.objects.get_for_model(self), - target_id=self.pk) + operation=operation, target_ct=ContentType.objects.get_for_model(self), target_id=self.pk + ) self.permissions.through.objects.get_or_create(role=self, permission=self_perm) return self_perm @@ -318,10 +324,12 @@ class Role(RoleAbstractBase): class Meta: verbose_name = _('role') verbose_name_plural = _('roles') - ordering = ('ou', 'service', 'name',) - unique_together = ( - ('admin_scope_ct', 'admin_scope_id'), + ordering = ( + 'ou', + 'service', + 'name', ) + unique_together = (('admin_scope_ct', 'admin_scope_id'),) def natural_key(self): return [ @@ -344,10 +352,13 @@ class Role(RoleAbstractBase): def export_json(self, attributes=False, parents=False, permissions=False): d = { - 'uuid': self.uuid, 'slug': self.slug, 'name': self.name, - 'description': self.description, 'external_id': self.external_id, + 'uuid': self.uuid, + 'slug': self.slug, + 'name': self.name, + 'description': self.description, + 'external_id': self.external_id, 'ou': self.ou and self.ou.natural_key_json(), - 'service': self.service and self.service.natural_key_json() + 'service': self.service and self.service.natural_key_json(), } if attributes: @@ -383,43 +394,30 @@ class RoleParenting(RoleParentingAbstractBase): verbose_name_plural = _('role parenting relations') def __str__(self): - return u'{0} {1}> {2}'.format(self.parent.name, '-' if self.direct else '~', - self.child.name) + return u'{0} {1}> {2}'.format(self.parent.name, '-' if self.direct else '~', self.child.name) class RoleAttribute(models.Model): - KINDS = ( - ('string', _('string')), - ) + KINDS = (('string', _('string')),) role = models.ForeignKey( - to=Role, - verbose_name=_('role'), - related_name='attributes', - on_delete=models.CASCADE) - name = models.CharField( - max_length=64, - verbose_name=_('name')) - kind = models.CharField( - max_length=32, - choices=KINDS, - verbose_name=_('kind')) - value = models.TextField( - verbose_name=_('value')) + to=Role, verbose_name=_('role'), related_name='attributes', on_delete=models.CASCADE + ) + name = models.CharField(max_length=64, verbose_name=_('name')) + kind = models.CharField(max_length=32, choices=KINDS, verbose_name=_('kind')) + value = models.TextField(verbose_name=_('value')) class Meta: - verbose_name = ('role attribute') + verbose_name = 'role attribute' verbose_name_plural = _('role attributes') - unique_together = ( - ('role', 'name', 'kind', 'value'), - ) + unique_together = (('role', 'name', 'kind', 'value'),) def to_json(self): return {'name': self.name, 'kind': self.kind, 'value': self.value} -GenericRelation(Permission, - content_type_field='target_ct', - object_id_field='target_id').contribute_to_class(ContentType, 'admin_perms') +GenericRelation(Permission, content_type_field='target_ct', object_id_field='target_id').contribute_to_class( + ContentType, 'admin_perms' +) CHANGE_PASSWORD_OP = Operation.register(name=_('Change password'), slug='change_password') @@ -427,5 +425,4 @@ RESET_PASSWORD_OP = Operation.register(name=_('Password reset'), slug='reset_pas ACTIVATE_OP = Operation.register(name=_('Activation'), slug='activate') CHANGE_EMAIL_OP = Operation.register(name=pgettext_lazy('operation', 'Change email'), slug='change_email') MANAGE_MEMBERS_OP = Operation.register(name=_('Manage role members'), slug='manage_members') -MANAGE_AUTHORIZATIONS_OP = Operation.register( - name=_('Manage service consents'), slug='manage_authorizations') +MANAGE_AUTHORIZATIONS_OP = Operation.register(name=_('Manage service consents'), slug='manage_authorizations') diff --git a/src/authentic2/a2_rbac/signal_handlers.py b/src/authentic2/a2_rbac/signal_handlers.py index b4f4ef13..2bb81ac5 100644 --- a/src/authentic2/a2_rbac/signal_handlers.py +++ b/src/authentic2/a2_rbac/signal_handlers.py @@ -25,8 +25,7 @@ from django_rbac.utils import get_ou_model, get_role_model, get_operation from django_rbac.managers import defer_update_transitive_closure -def create_default_ou(app_config, verbosity=2, interactive=True, - using=DEFAULT_DB_ALIAS, **kwargs): +def create_default_ou(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): if not router.allow_migrate(using, get_ou_model()): return # be sure new objects names are localized using the default locale @@ -40,7 +39,8 @@ def create_default_ou(app_config, verbosity=2, interactive=True, defaults={ 'default': True, 'name': _('Default organizational unit'), - }) + }, + ) # Update all existing models having an ou field to the default ou for app in apps.get_app_configs(): for model in app.get_models(): @@ -50,8 +50,7 @@ def create_default_ou(app_config, verbosity=2, interactive=True, model.objects.filter(ou__isnull=True).update(ou=default_ou) -def post_migrate_update_rbac(app_config, verbosity=2, interactive=True, - using=DEFAULT_DB_ALIAS, **kwargs): +def post_migrate_update_rbac(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # be sure new objects names are localized using the default locale from .management import update_ous_admin_roles, update_content_types_roles @@ -84,10 +83,15 @@ def update_service_role_ou(sender, instance, created, raw, **kwargs): get_role_model().objects.filter(service=instance).update(ou=instance.ou) -def create_default_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, - **kwargs): - from .models import (CHANGE_PASSWORD_OP, RESET_PASSWORD_OP, ACTIVATE_OP, CHANGE_EMAIL_OP, - MANAGE_MEMBERS_OP, MANAGE_AUTHORIZATIONS_OP) +def create_default_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): + from .models import ( + CHANGE_PASSWORD_OP, + RESET_PASSWORD_OP, + ACTIVATE_OP, + CHANGE_EMAIL_OP, + MANAGE_MEMBERS_OP, + MANAGE_AUTHORIZATIONS_OP, + ) if not router.allow_migrate(using, get_ou_model()): return diff --git a/src/authentic2/a2_rbac/utils.py b/src/authentic2/a2_rbac/utils.py index 8af2d4b27..468aed04 100644 --- a/src/authentic2/a2_rbac/utils.py +++ b/src/authentic2/a2_rbac/utils.py @@ -37,7 +37,9 @@ def get_view_user_perm(ou=None): operation=rbac_utils.get_operation(VIEW_OP), target_ct=ContentType.objects.get_for_model(ContentType), target_id=ContentType.objects.get_for_model(User).pk, - ou__isnull=ou is None, ou=ou) + ou__isnull=ou is None, + ou=ou, + ) return view_user_perm @@ -48,7 +50,8 @@ def get_search_ou_perm(ou=None): operation=rbac_utils.get_operation(SEARCH_OP), target_ct=ContentType.objects.get_for_model(ou), target_id=ou.pk, - ou__isnull=True) + ou__isnull=True, + ) else: OU = rbac_utils.get_ou_model() Permission = rbac_utils.get_permission_model() @@ -56,7 +59,8 @@ def get_search_ou_perm(ou=None): operation=rbac_utils.get_operation(SEARCH_OP), target_ct=ContentType.objects.get_for_model(ContentType), target_id=ContentType.objects.get_for_model(OU).pk, - ou__isnull=True) + ou__isnull=True, + ) return view_ou_perm @@ -67,5 +71,7 @@ def get_manage_authorizations_user_perm(ou=None): operation=rbac_utils.get_operation(models.MANAGE_AUTHORIZATIONS_OP), target_ct=ContentType.objects.get_for_model(ContentType), target_id=ContentType.objects.get_for_model(User).pk, - ou__isnull=ou is None, ou=ou) + ou__isnull=ou is None, + ou=ou, + ) return manage_authorizations_user_perm diff --git a/src/authentic2/admin.py b/src/authentic2/admin.py index f003c45a..f0d07517 100644 --- a/src/authentic2/admin.py +++ b/src/authentic2/admin.py @@ -30,14 +30,15 @@ from django import forms from django.contrib.auth.forms import ReadOnlyPasswordHashField from .nonce.models import Nonce -from . import (models, app_settings, decorators, attribute_kinds, - utils) +from . import models, app_settings, decorators, attribute_kinds, utils from .forms.profile import BaseUserForm, modelform_factory from .custom_user.models import User, DeletedUser def cleanup_action(modeladmin, request, queryset): queryset.cleanup() + + cleanup_action.short_description = _('Cleanup expired objects') @@ -52,18 +53,21 @@ class CleanupAdminMixin(admin.ModelAdmin): class NonceModelAdmin(admin.ModelAdmin): list_display = ("value", "context", "not_on_or_after") + admin.site.register(Nonce, NonceModelAdmin) class AttributeValueAdmin(admin.ModelAdmin): list_display = ('content_type', 'owner', 'attribute', 'content') + admin.site.register(models.AttributeValue, AttributeValueAdmin) class LogoutUrlAdmin(admin.ModelAdmin): list_display = ('provider', 'logout_url', 'logout_use_iframe', 'logout_use_iframe_timeout') + admin.site.register(models.LogoutUrl, LogoutUrlAdmin) @@ -73,6 +77,7 @@ class AuthenticationEventAdmin(admin.ModelAdmin): date_hierarchy = 'when' search_fields = ('who', 'nonce', 'how') + admin.site.register(models.AuthenticationEvent, AuthenticationEventAdmin) @@ -82,6 +87,7 @@ class UserExternalIdAdmin(admin.ModelAdmin): date_hierarchy = 'created' search_fields = ('user__username', 'source', 'external_id') + admin.site.register(models.UserExternalId, UserExternalIdAdmin) @@ -92,9 +98,11 @@ DB_SESSION_ENGINES = ( ) if settings.SESSION_ENGINE in DB_SESSION_ENGINES: + class SessionAdmin(admin.ModelAdmin): def _session_data(self, obj): return pprint.pformat(obj.get_decoded()).replace('\n', '
\n') + _session_data.allow_tags = True _session_data.short_description = _('session data') list_display = ['session_key', 'ips', 'user', '_session_data', 'expire_date'] @@ -107,11 +115,13 @@ if settings.SESSION_ENGINE in DB_SESSION_ENGINES: content = session.get_decoded() ips = content.get('ips', set()) return ', '.join(ips) + ips.short_description = _('IP adresses') def user(self, session): from django.contrib import auth from django.contrib.auth import models as auth_models + content = session.get_decoded() if auth.SESSION_KEY not in content: return @@ -125,10 +135,12 @@ if settings.SESSION_ENGINE in DB_SESSION_ENGINES: except Exception: user = _('deleted user %r') % user_id return user + user.short_description = _('user') def clear_expired(self, request, queryset): queryset.filter(expire_date__lt=timezone.now()).delete() + clear_expired.short_description = _('clear expired sessions') admin.site.register(Session, SessionAdmin) @@ -140,10 +152,7 @@ class ExternalUserListFilter(admin.SimpleListFilter): parameter_name = 'external' def lookups(self, request, model_admin): - return ( - ('1', _('Yes')), - ('0', _('No')) - ) + return (('1', _('Yes')), ('0', _('No'))) def queryset(self, request, queryset): """ @@ -194,9 +203,12 @@ class UserChangeForm(BaseUserForm): password = ReadOnlyPasswordHashField( label=_("Password"), - help_text=_("Raw passwords are not stored, so there is no way to see " - "this user's password, but you can change the password " - "using this form.")) + help_text=_( + "Raw passwords are not stored, so there is no way to see " + "this user's password, but you can change the password " + "using this form." + ), + ) class Meta: model = User @@ -230,17 +242,17 @@ class UserCreationForm(BaseUserForm): A form that creates a user, with no privileges, from the given username and password. """ + error_messages = { 'password_mismatch': _("The two password fields didn't match."), 'missing_credential': _("You must at least give a username or an email to your user"), } - password1 = forms.CharField( - label=_("Password"), - widget=forms.PasswordInput) + password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) password2 = forms.CharField( label=_("Password confirmation"), widget=forms.PasswordInput, - help_text=_("Enter the same password as above, for verification.")) + help_text=_("Enter the same password as above, for verification."), + ) class Meta: model = User @@ -275,14 +287,17 @@ class AuthenticUserAdmin(UserAdmin): fieldsets = ( (None, {'fields': ('uuid', 'ou', 'password')}), (_('Personal info'), {'fields': ('username', 'first_name', 'last_name', 'email')}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', - 'groups')}), + (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}), (_('Important dates'), {'fields': ('last_login', 'date_joined', 'deactivation')}), ) add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2')}), + ( + None, + { + 'classes': ('wide',), + 'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2'), + }, + ), ) readonly_fields = ('uuid',) list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter) @@ -303,17 +318,22 @@ class AuthenticUserAdmin(UserAdmin): fieldsets = list(fieldsets) fieldsets.insert( insertion_idx, - (_('Attributes'), {'fields': [at.name for at in qs if at.name not in - ['first_name', 'last_name']]})) + ( + _('Attributes'), + {'fields': [at.name for at in qs if at.name not in ['first_name', 'last_name']]}, + ), + ) return fieldsets def get_form(self, request, obj=None, **kwargs): - self.form = modelform_factory(self.model, form=UserChangeForm, - fields=models.Attribute.objects.values_list('name', - flat=True)) - self.add_form = modelform_factory(self.model, form=UserCreationForm, - fields=models.Attribute.objects.filter(required=True) - .values_list('name', flat=True)) + self.form = modelform_factory( + self.model, form=UserChangeForm, fields=models.Attribute.objects.values_list('name', flat=True) + ) + self.add_form = modelform_factory( + self.model, + form=UserCreationForm, + fields=models.Attribute.objects.filter(required=True).values_list('name', flat=True), + ) if 'fields' in kwargs: fields = kwargs.pop('fields') else: @@ -332,8 +352,10 @@ class AuthenticUserAdmin(UserAdmin): timestamp = timezone.now() for user in queryset: user.mark_as_inactive(timestamp=timestamp) + mark_as_inactive.short_description = _('Mark as inactive') + admin.site.register(User, AuthenticUserAdmin) @@ -355,13 +377,23 @@ class AttributeForm(forms.ModelForm): class AttributeAdmin(admin.ModelAdmin): form = AttributeForm - list_display = ('label', 'disabled', 'name', 'kind', 'order', 'required', - 'asked_on_registration', 'user_editable', 'user_visible') + list_display = ( + 'label', + 'disabled', + 'name', + 'kind', + 'order', + 'required', + 'asked_on_registration', + 'user_editable', + 'user_visible', + ) list_editable = ('order',) def get_queryset(self, request): return self.model.all_objects.all() + admin.site.register(models.Attribute, AttributeAdmin) @@ -370,6 +402,7 @@ class DeletedUserAdmin(admin.ModelAdmin): date_hierarchy = 'deleted' search_fields = ['=old_user_id', '^old_uuid', 'old_email'] + admin.site.register(DeletedUser, DeletedUserAdmin) @@ -377,6 +410,7 @@ admin.site.register(DeletedUser, DeletedUserAdmin) def login(request, extra_context=None): return utils.redirect_to_login(request, login_url=utils.get_manager_login_url()) + admin.site.login = login @@ -384,6 +418,7 @@ admin.site.login = login def logout(request, extra_context=None): return utils.redirect_to_login(request, login_url='auth_logout') + admin.site.logout = logout admin.site.register(models.PasswordReset) diff --git a/src/authentic2/api_mixins.py b/src/authentic2/api_mixins.py index 18896ec4..41d5df1e 100644 --- a/src/authentic2/api_mixins.py +++ b/src/authentic2/api_mixins.py @@ -55,8 +55,9 @@ class GetOrCreateMixinView(object): except qs.model.DoesNotExist: return None except qs.model.MultipleObjectsReturned: - raise Conflict('retrieved several instances of model %s for key attributes %s' % ( - qs.model.__name__, kwargs)) + raise Conflict( + 'retrieved several instances of model %s for key attributes %s' % (qs.model.__name__, kwargs) + ) def _validate_get_keys(self, keys): # Remove many-to-many relationships from validated_data. diff --git a/src/authentic2/api_urls.py b/src/authentic2/api_urls.py index 9aab37db..61734faf 100644 --- a/src/authentic2/api_urls.py +++ b/src/authentic2/api_urls.py @@ -22,10 +22,16 @@ urlpatterns = [ url(r'^register/$', api_views.register, name='a2-api-register'), url(r'^password-change/$', api_views.password_change, name='a2-api-password-change'), url(r'^user/$', api_views.user, name='a2-api-user'), - url(r'^roles/(?P[\w+]*)/members/(?P[^/]+)/$', api_views.role_membership, - name='a2-api-role-member'), - url(r'^roles/(?P[\w+]*)/relationships/members/$', api_views.role_memberships, - name='a2-api-role-members'), + url( + r'^roles/(?P[\w+]*)/members/(?P[^/]+)/$', + api_views.role_membership, + name='a2-api-role-member', + ), + url( + r'^roles/(?P[\w+]*)/relationships/members/$', + api_views.role_memberships, + name='a2-api-role-members', + ), url(r'^check-password/$', api_views.check_password, name='a2-api-check-password'), url(r'^validate-password/$', api_views.validate_password, name='a2-api-validate-password'), url(r'^address-autocomplete/$', api_views.address_autocomplete, name='a2-api-address-autocomplete'), diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py index 9485edf9..f92e7cf1 100644 --- a/src/authentic2/api_views.py +++ b/src/authentic2/api_views.py @@ -46,8 +46,7 @@ from rest_framework.routers import SimpleRouter from rest_framework.generics import GenericAPIView from rest_framework.response import Response from rest_framework import permissions, status, authentication -from rest_framework.exceptions import (PermissionDenied, AuthenticationFailed, - ValidationError, NotFound) +from rest_framework.exceptions import PermissionDenied, AuthenticationFailed, ValidationError, NotFound from rest_framework.fields import CreateOnlyDefault from authentic2.compat.drf import action from rest_framework.authentication import SessionAuthentication @@ -61,8 +60,7 @@ from django_filters.utils import handle_timezone from .passwords import get_password_checker from .custom_user.models import User -from . import (utils, decorators, attribute_kinds, app_settings, hooks, - api_mixins) +from . import utils, decorators, attribute_kinds, app_settings, hooks, api_mixins from .models import Attribute, PasswordReset, Service from .a2_rbac.utils import get_default_ou from .journal_event_types import UserLogin, UserRegistration @@ -73,6 +71,7 @@ from .utils.lookups import Unaccent if django.VERSION < (2,): import rest_framework.fields from . import validators + rest_framework.fields.ProhibitNullCharactersValidator = validators.ProhibitNullCharactersValidator if django.VERSION < (1, 11): authentication.authenticate = utils.authenticate @@ -128,23 +127,20 @@ class ExceptionHandlerMixin(object): class RegistrationSerializer(serializers.Serializer): '''Register RPC payload''' - email = serializers.EmailField( - required=False, allow_blank=True) + + email = serializers.EmailField(required=False, allow_blank=True) ou = serializers.SlugRelatedField( queryset=get_ou_model().objects.all(), slug_field='slug', default=get_default_ou, - required=False, allow_null=True) - username = serializers.CharField( - required=False, allow_blank=True) - first_name = serializers.CharField( - required=False, allow_blank=True, default='') - last_name = serializers.CharField( - required=False, allow_blank=True, default='') - password = serializers.CharField( - required=False, allow_null=True) - no_email_validation = serializers.BooleanField( - required=False) + required=False, + allow_null=True, + ) + username = serializers.CharField(required=False, allow_blank=True) + first_name = serializers.CharField(required=False, allow_blank=True, default='') + last_name = serializers.CharField(required=False, allow_blank=True, default='') + password = serializers.CharField(required=False, allow_null=True) + no_email_validation = serializers.BooleanField(required=False) return_url = serializers.URLField(required=False, allow_blank=True) def validate(self, data): @@ -157,48 +153,34 @@ class RegistrationSerializer(serializers.Serializer): else: authorized = request.user.has_perm(perm) if not authorized: - raise serializers.ValidationError(_('you are not authorized ' - 'to create users in ' - 'this ou')) + raise serializers.ValidationError( + _('you are not authorized ' 'to create users in ' 'this ou') + ) User = get_user_model() if ou: - if (app_settings.A2_EMAIL_IS_UNIQUE or - app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE): + if app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE: if 'email' not in data: - raise serializers.ValidationError( - _('Email is required')) - if User.objects.filter( - email__iexact=data['email']).exists(): - raise serializers.ValidationError( - _('Account already exists')) + raise serializers.ValidationError(_('Email is required')) + if User.objects.filter(email__iexact=data['email']).exists(): + raise serializers.ValidationError(_('Account already exists')) if ou.email_is_unique: if 'email' not in data: - raise serializers.ValidationError( - _('Email is required in this ou')) - if User.objects.filter( - ou=ou, email__iexact=data['email']).exists(): - raise serializers.ValidationError( - _('Account already exists in this ou')) - - if (app_settings.A2_USERNAME_IS_UNIQUE or - app_settings.A2_REGISTRATION_USERNAME_IS_UNIQUE): + raise serializers.ValidationError(_('Email is required in this ou')) + if User.objects.filter(ou=ou, email__iexact=data['email']).exists(): + raise serializers.ValidationError(_('Account already exists in this ou')) + + if app_settings.A2_USERNAME_IS_UNIQUE or app_settings.A2_REGISTRATION_USERNAME_IS_UNIQUE: if 'username' not in data: - raise serializers.ValidationError( - _('Username is required')) - if User.objects.filter( - username=data['username']).exists(): - raise serializers.ValidationError( - _('Account already exists')) + raise serializers.ValidationError(_('Username is required')) + if User.objects.filter(username=data['username']).exists(): + raise serializers.ValidationError(_('Account already exists')) if ou.username_is_unique: if 'username' not in data: - raise serializers.ValidationError( - _('Username is required in this ou')) - if User.objects.filter( - ou=ou, username=data['username']).exists(): - raise serializers.ValidationError( - _('Account already exists in this ou')) + raise serializers.ValidationError(_('Username is required in this ou')) + if User.objects.filter(ou=ou, username=data['username']).exists(): + raise serializers.ValidationError(_('Account already exists in this ou')) return data @@ -218,27 +200,29 @@ class BaseRpcView(ExceptionHandlerMixin, RpcMixin, GenericAPIView): class Register(BaseRpcView): - '''Register the given email, send a mail to the user and return a - validation token. - - A mail will be sent to the user to validate its email. On - validation of the mail the user will be logged and redirected to - `{return_url}?token={token}`. It's the durty of the requesting - service to finish the registration process on its side. - - If email is unique and an account already exist the requesting - must enter in a process of registration through SSO, i.e. ask for - authentication of the user and then finish the registration - process for the received identity. - ''' + """Register the given email, send a mail to the user and return a + validation token. + + A mail will be sent to the user to validate its email. On + validation of the mail the user will be logged and redirected to + `{return_url}?token={token}`. It's the durty of the requesting + service to finish the registration process on its side. + + If email is unique and an account already exist the requesting + must enter in a process of registration through SSO, i.e. ask for + authentication of the user and then finish the registration + process for the received identity. + """ + permission_classes = (permissions.IsAuthenticated,) serializer_class = RegistrationSerializer def rpc(self, request, serializer): validated_data = serializer.validated_data if not request.user.has_ou_perm('custom_user.add_user', validated_data['ou']): - raise PermissionDenied('You do not have permission to create users in ou %s' % - validated_data['ou'].slug) + raise PermissionDenied( + 'You do not have permission to create users in ou %s' % validated_data['ou'].slug + ) email = validated_data.get('email') registration_data = {} for field in ('first_name', 'last_name', 'password', 'username'): @@ -255,28 +239,27 @@ class Register(BaseRpcView): final_return_url = None if validated_data.get('return_url'): token = utils.get_hex_uuid()[:16] - final_return_url = utils.make_url(validated_data['return_url'], - params={'token': token}) + final_return_url = utils.make_url(validated_data['return_url'], params={'token': token}) if email and not validated_data.get('no_email_validation'): registration_template = ['authentic2/activation_email'] if validated_data['ou']: - registration_template.insert(0, 'authentic2/activation_email_%s' % - validated_data['ou'].slug) + registration_template.insert(0, 'authentic2/activation_email_%s' % validated_data['ou'].slug) try: - utils.send_registration_mail(self.request, email, - template_names=registration_template, - next_url=final_return_url, - ou=validated_data['ou'], - context=ctx, - **registration_data) + utils.send_registration_mail( + self.request, + email, + template_names=registration_template, + next_url=final_return_url, + ou=validated_data['ou'], + context=ctx, + **registration_data, + ) except smtplib.SMTPException as e: response = { 'result': 0, - 'errors': { - '__all__': ['Mail sending failed'] - }, + 'errors': {'__all__': ['Mail sending failed']}, 'exception': force_text(e), } response_status = status.HTTP_503_SERVICE_UNAVAILABLE @@ -293,20 +276,20 @@ class Register(BaseRpcView): last_name = validated_data.get('last_name') password = validated_data.get('password') ou = validated_data.get('ou') - if not email and \ - not username and \ - not (first_name and last_name): + if not email and not username and not (first_name and last_name): response = { 'result': 0, 'errors': { - '__all__': ['You must set at least a username, an email or ' - 'a first name and a last name'] + '__all__': [ + 'You must set at least a username, an email or ' 'a first name and a last name' + ] }, } response_status = status.HTTP_400_BAD_REQUEST else: - new_user = User(email=email, username=username, ou=ou, first_name=first_name, - last_name=last_name) + new_user = User( + email=email, username=username, ou=ou, first_name=first_name, last_name=last_name + ) if password: new_user.set_password(password) new_user.save() @@ -318,26 +301,26 @@ class Register(BaseRpcView): } if email: response['validation_url'] = utils.build_activation_url( - request, email, next_url=final_return_url, ou=ou, **registration_data) + request, email, next_url=final_return_url, ou=ou, **registration_data + ) if token: response['token'] = token response_status = status.HTTP_201_CREATED return response, response_status + register = Register.as_view() class PasswordChangeSerializer(serializers.Serializer): '''Register RPC payload''' + email = serializers.EmailField() ou = serializers.SlugRelatedField( - queryset=get_ou_model().objects.all(), - slug_field='slug', - required=False, allow_null=True) - old_password = serializers.CharField( - required=True, allow_null=True) - new_password = serializers.CharField( - required=True, allow_null=True) + queryset=get_ou_model().objects.all(), slug_field='slug', required=False, allow_null=True + ) + old_password = serializers.CharField(required=True, allow_null=True) + new_password = serializers.CharField(required=True, allow_null=True) def validate(self, data): User = get_user_model() @@ -364,6 +347,7 @@ class PasswordChange(BaseRpcView): serializer.user.save() return {'result': 1}, status.HTTP_200_OK + password_change = PasswordChange.as_view() @@ -378,14 +362,12 @@ def user(request): class BaseUserSerializer(serializers.ModelSerializer): ou = serializers.SlugRelatedField( - queryset=get_ou_model().objects.all(), - slug_field='slug', - required=False, default=get_default_ou) + queryset=get_ou_model().objects.all(), slug_field='slug', required=False, default=get_default_ou + ) date_joined = serializers.DateTimeField(read_only=True) last_login = serializers.DateTimeField(read_only=True) dist = serializers.FloatField(read_only=True) - send_registration_email = serializers.BooleanField(write_only=True, required=False, - default=False) + send_registration_email = serializers.BooleanField(write_only=True, required=False, default=False) send_registration_email_next_url = serializers.URLField(write_only=True, required=False) password = serializers.CharField(max_length=128, required=False) force_password_reset = serializers.BooleanField(write_only=True, required=False, default=False) @@ -402,7 +384,8 @@ class BaseUserSerializer(serializers.ModelSerializer): else: self.fields[at.name] = at.get_drf_field() self.fields[at.name + '_verified'] = serializers.BooleanField( - source='is_verified.%s' % at.name, required=False) + source='is_verified.%s' % at.name, required=False + ) for key in self.fields: if key in app_settings.A2_REQUIRED_FIELDS: self.fields[key].required = True @@ -418,8 +401,7 @@ class BaseUserSerializer(serializers.ModelSerializer): def create(self, validated_data): original_data = validated_data.copy() send_registration_email = validated_data.pop('send_registration_email', False) - send_registration_email_next_url = validated_data.pop('send_registration_email_next_url', - None) + send_registration_email_next_url = validated_data.pop('send_registration_email_next_url', None) force_password_reset = validated_data.pop('force_password_reset', False) attributes = validated_data.pop('attributes', {}) @@ -454,16 +436,20 @@ class BaseUserSerializer(serializers.ModelSerializer): try: utils.send_password_reset_mail( instance, - template_names=['authentic2/api_user_create_registration_email', - 'authentic2/password_reset'], + template_names=[ + 'authentic2/api_user_create_registration_email', + 'authentic2/password_reset', + ], request=self.context['request'], next_url=send_registration_email_next_url, context={ 'data': original_data, - }) + }, + ) except smtplib.SMTPException as e: - logging.getLogger(__name__).error(u'registration mail could not be sent to user %s ' - 'created through API: %s', instance, e) + logging.getLogger(__name__).error( + u'registration mail could not be sent to user %s ' 'created through API: %s', instance, e + ) return instance def update(self, instance, validated_data): @@ -479,8 +465,7 @@ class BaseUserSerializer(serializers.ModelSerializer): self.check_perm('custom_user.change_user', instance.ou) if 'ou' in validated_data: self.check_perm('custom_user.change_user', validated_data.get('ou')) - if validated_data.get('email') != instance.email and \ - not validated_data.get('email_verified'): + if validated_data.get('email') != instance.email and not validated_data.get('email_verified'): instance.email_verified = False super(BaseUserSerializer, self).update(instance, validated_data) for key, value in attributes.items(): @@ -523,15 +508,15 @@ class BaseUserSerializer(serializers.ModelSerializer): update_or_create_fields = self.context['view'].request.GET.getlist('update_or_create') already_used = False - if ('email' not in get_or_create_fields - and 'email' not in update_or_create_fields - and data.get('email') - and (not self.instance or data.get('email') != self.instance.email)): - if app_settings.A2_EMAIL_IS_UNIQUE and qs.filter( - email=data['email']).exists(): + if ( + 'email' not in get_or_create_fields + and 'email' not in update_or_create_fields + and data.get('email') + and (not self.instance or data.get('email') != self.instance.email) + ): + if app_settings.A2_EMAIL_IS_UNIQUE and qs.filter(email=data['email']).exists(): already_used = True - if ou and ou.email_is_unique and qs.filter( - ou=ou, email=data['email']).exists(): + if ou and ou.email_is_unique and qs.filter(ou=ou, email=data['email']).exists(): already_used = True errors = {} @@ -587,12 +572,11 @@ class RoleSerializer(serializers.ModelSerializer): required=False, default=CreateOnlyDefault(get_default_ou), queryset=get_ou_model().objects.all(), - slug_field='slug') + slug_field='slug', + ) slug = serializers.SlugField( - required=False, - allow_blank=False, - max_length=256, - default=SlugFromNameDefault()) + required=False, allow_blank=False, max_length=256, default=SlugFromNameDefault() + ) @property def user(self): @@ -626,17 +610,16 @@ class RoleSerializer(serializers.ModelSerializer): class Meta: model = get_role_model() - fields = ('uuid', 'name', 'slug', 'ou',) + fields = ( + 'uuid', + 'name', + 'slug', + 'ou', + ) extra_kwargs = {'uuid': {'read_only': True}} validators = [ - UniqueTogetherValidator( - queryset=get_role_model().objects.all(), - fields=['name', 'ou'] - ), - UniqueTogetherValidator( - queryset=get_role_model().objects.all(), - fields=['slug', 'ou'] - ) + UniqueTogetherValidator(queryset=get_role_model().objects.all(), fields=['name', 'ou']), + UniqueTogetherValidator(queryset=get_role_model().objects.all(), fields=['slug', 'ou']), ] @@ -652,10 +635,12 @@ class IsoDateTimeField(IsoDateTimeField): return super(IsoDateTimeField, self).strptime(value, format) except AmbiguousTimeError: parsed = parse_datetime(value) - possible = sorted([ - handle_timezone(parsed, is_dst=True), - handle_timezone(parsed, is_dst=False), - ]) + possible = sorted( + [ + handle_timezone(parsed, is_dst=True), + handle_timezone(parsed, is_dst=False), + ] + ) if self.bound == 'lesser': return possible[0] elif self.bound == 'upper': @@ -680,10 +665,7 @@ class UsersFilter(FilterSet): class Meta: model = get_user_model() fields = { - 'username': [ - 'exact', - 'iexact' - ], + 'username': ['exact', 'iexact'], 'first_name': [ 'exact', 'iexact', @@ -728,8 +710,8 @@ class ChangeEmailSerializer(serializers.Serializer): class FreeTextSearchFilter(BaseFilterBackend): - """ - """ + """""" + def filter_queryset(self, request, queryset, view): if 'q' in request.GET: queryset = queryset.free_text_search(request.GET['q']) @@ -753,9 +735,9 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin @property def ordering(self): - if 'q' in self.request.GET: - return ['dist', Unaccent('last_name'), Unaccent('first_name')] - return User._meta.ordering + if 'q' in self.request.GET: + return ['dist', Unaccent('last_name'), Unaccent('first_name')] + return User._meta.ordering def get_queryset(self): qs = super().get_queryset() @@ -773,10 +755,11 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin if 'service-slug' in self.request.GET: service_slug = self.request.GET['service-slug'] service_ou = self.request.GET.get('service-ou', '') - service = Service.objects.filter( - slug=service_slug, - ou__slug=service_ou - ).prefetch_related('authorized_roles').first() + service = ( + Service.objects.filter(slug=service_slug, ou__slug=service_ou) + .prefetch_related('authorized_roles') + .first() + ) if service: if service.authorized_roles.all(): qs = qs.filter(roles__in=service.authorized_roles.children()) @@ -809,15 +792,11 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin known_uuids = User.objects.filter(uuid__in=uuids).values_list('uuid', flat=True) return set(uuids) - set(known_uuids) - @action(detail=False, methods=['post'], - permission_classes=(DjangoPermission('custom_user.search_user'),)) + @action(detail=False, methods=['post'], permission_classes=(DjangoPermission('custom_user.search_user'),)) def synchronization(self, request): serializer = self.SynchronizationSerializer(data=request.data) if not serializer.is_valid(): - response = { - 'result': 0, - 'errors': serializer.errors - } + response = {'result': 0, 'errors': serializer.errors} return Response(response, status.HTTP_400_BAD_REQUEST) hooks.call_hooks('api_modify_serializer_after_validation', self, serializer) unknown_uuids = self.check_uuids(serializer.validated_data.get('known_uuids', [])) @@ -828,43 +807,40 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin hooks.call_hooks('api_modify_response', self, 'synchronization', data) return Response(data) - @action(detail=True, methods=['post'], url_path='password-reset', - permission_classes=(DjangoPermission('custom_user.reset_password_user'),)) + @action( + detail=True, + methods=['post'], + url_path='password-reset', + permission_classes=(DjangoPermission('custom_user.reset_password_user'),), + ) def password_reset(self, request, uuid): user = self.get_object() # An user without email cannot receive the token if not user.email: - return Response({'result': 0, 'reason': 'User has no mail'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {'result': 0, 'reason': 'User has no mail'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) utils.send_password_reset_mail(user, request=request) return Response(status=status.HTTP_204_NO_CONTENT) - @action(detail=True, methods=['post'], - permission_classes=(DjangoPermission('custom_user.change_user'),)) + @action(detail=True, methods=['post'], permission_classes=(DjangoPermission('custom_user.change_user'),)) def email(self, request, uuid): user = self.get_object() serializer = ChangeEmailSerializer(data=request.data) if not serializer.is_valid(): - response = { - 'result': 0, - 'errors': serializer.errors - } + response = {'result': 0, 'errors': serializer.errors} return Response(response, status.HTTP_400_BAD_REQUEST) user.email_verified = False user.save() utils.send_email_change_email(user, serializer.validated_data['email'], request=request) return Response({'result': 1}) - @action(detail=False, methods=['get'], - permission_classes=(DjangoPermission('custom_user.search_user'),)) + @action(detail=False, methods=['get'], permission_classes=(DjangoPermission('custom_user.search_user'),)) def find_duplicates(self, request): serializer = self.get_serializer(data=request.query_params, partial=True) if not serializer.is_valid(): - response = { - 'data': [], - 'err': 1, - 'err_desc': serializer.errors - } + response = {'data': [], 'err': 1, 'err_desc': serializer.errors} return Response(response, status.HTTP_400_BAD_REQUEST) data = serializer.validated_data @@ -882,10 +858,12 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin birthdate = attributes.get('birthdate') qs = User.objects.find_duplicates(first_name, last_name, birthdate=birthdate) - return Response({ - 'data': DuplicateUserSerializer(qs, many=True).data, - 'err': 0, - }) + return Response( + { + 'data': DuplicateUserSerializer(qs, many=True).data, + 'err': 0, + } + ) class RolesAPI(api_mixins.GetOrCreateMixinView, ExceptionHandlerMixin, ModelViewSet): @@ -919,13 +897,16 @@ class RoleMembershipAPI(ExceptionHandlerMixin, APIView): def post(self, request, *args, **kwargs): self.role.members.add(self.member) - return Response({'result': 1, 'detail': _('User successfully added to role')}, - status=status.HTTP_201_CREATED) + return Response( + {'result': 1, 'detail': _('User successfully added to role')}, status=status.HTTP_201_CREATED + ) def delete(self, request, *args, **kwargs): self.role.members.remove(self.member) - return Response({'result': 1, 'detail': _('User successfully removed from role')}, - status=status.HTTP_200_OK) + return Response( + {'result': 1, 'detail': _('User successfully removed from role')}, status=status.HTTP_200_OK + ) + role_membership = RoleMembershipAPI.as_view() @@ -956,16 +937,15 @@ class RoleMembershipsAPI(ExceptionHandlerMixin, APIView): try: uuid = entry['uuid'] except TypeError: - raise ValidationError(_("List elements of the 'data' dict " - "entry must be dictionaries")) + raise ValidationError(_("List elements of the 'data' dict " "entry must be dictionaries")) except KeyError: - raise ValidationError(_("Missing 'uuid' key for dict entry %s " - "of the 'data' payload") % entry) + raise ValidationError( + _("Missing 'uuid' key for dict entry %s " "of the 'data' payload") % entry + ) try: self.members.append(User.objects.get(uuid=uuid)) except User.DoesNotExist: - raise ValidationError( - _('No known user for UUID %s') % entry['uuid']) + raise ValidationError(_('No known user for UUID %s') % entry['uuid']) if not len(self.members) and request.method in ('POST', 'DELETE'): raise ValidationError(_('No valid user UUID')) @@ -973,43 +953,36 @@ class RoleMembershipsAPI(ExceptionHandlerMixin, APIView): def post(self, request, *args, **kwargs): self.role.members.add(*self.members) return Response( - { - 'result': 1, - 'detail': _('Users successfully added to role') - }, - status=status.HTTP_201_CREATED) + {'result': 1, 'detail': _('Users successfully added to role')}, status=status.HTTP_201_CREATED + ) def delete(self, request, *args, **kwargs): self.role.members.remove(*self.members) return Response( - { - 'result': 1, - 'detail': _('Users successfully removed from role') - }, - status=status.HTTP_200_OK) + {'result': 1, 'detail': _('Users successfully removed from role')}, status=status.HTTP_200_OK + ) def patch(self, request, *args, **kwargs): self.role.members.set(self.members) return Response( - { - 'result': 1, - 'detail': _('Users successfully assigned to role') - }, - status=status.HTTP_200_OK) + {'result': 1, 'detail': _('Users successfully assigned to role')}, status=status.HTTP_200_OK + ) def put(self, request, *args, **kwargs): return self.patch(request, *args, **kwargs) + role_memberships = RoleMembershipsAPI.as_view() class BaseOrganizationalUnitSerializer(serializers.ModelSerializer): slug = serializers.SlugField( - required=False, - allow_blank=False, - max_length=256, - default=SlugFromNameDefault(), - ) + required=False, + allow_blank=False, + max_length=256, + default=SlugFromNameDefault(), + ) + class Meta: model = get_ou_model() fields = '__all__' @@ -1023,6 +996,7 @@ class OrganizationalUnitAPI(api_mixins.GetOrCreateMixinView, ExceptionHandlerMix def get_queryset(self): return get_ou_model().objects.all() + router = SimpleRouter() router.register(r'users', UsersAPI, base_name='a2-api-users') router.register(r'ous', OrganizationalUnitAPI, base_name='a2-api-ous') @@ -1046,7 +1020,8 @@ class CheckPasswordAPI(BaseRpcView): if hasattr(authenticator, 'authenticate_credentials'): try: user, oidc_client = authenticator.authenticate_credentials( - username, password, request=request) + username, password, request=request + ) result['result'] = 1 if hasattr(user, 'oidc_client'): result['oidc_client'] = True @@ -1056,6 +1031,7 @@ class CheckPasswordAPI(BaseRpcView): result['errors'] = [exc.detail] return result, status.HTTP_200_OK + check_password = CheckPasswordAPI.as_view() @@ -1080,13 +1056,16 @@ class ValidatePasswordAPI(BaseRpcView): ok = True for check in password_checker(serializer.validated_data['password']): ok = ok and check.result - checks.append({ - 'result': check.result, - 'label': check.label, - }) + checks.append( + { + 'result': check.result, + 'label': check.label, + } + ) result['ok'] = ok return result, status.HTTP_200_OK + validate_password = ValidatePasswordAPI.as_view() @@ -1097,10 +1076,7 @@ class AddressAutocompleteAPI(APIView): if not getattr(settings, 'ADDRESS_AUTOCOMPLETE_URL', None): return Response({}) try: - response = requests.get( - settings.ADDRESS_AUTOCOMPLETE_URL, - params=request.GET - ) + response = requests.get(settings.ADDRESS_AUTOCOMPLETE_URL, params=request.GET) response.raise_for_status() return Response(response.json()) except RequestException: @@ -1119,11 +1095,7 @@ class ServiceOUField(serializers.ListField): class StatisticsSerializer(serializers.Serializer): - TIME_INTERVAL_CHOICES = [ - ('day', _('Day')), - ('month', _('Month')), - ('year', _('Year')) - ] + TIME_INTERVAL_CHOICES = [('day', _('Day')), ('month', _('Month')), ('year', _('Year'))] time_interval = serializers.ChoiceField(choices=TIME_INTERVAL_CHOICES, default='month') service = ServiceOUField(child=serializers.SlugField(max_length=256), required=False) @@ -1143,6 +1115,7 @@ def stat(**kwargs): def wraps(func): func.filters = filters return decorator(func) + return wraps @@ -1169,7 +1142,9 @@ class StatisticsAPI(ViewSet): { 'id': 'time_interval', 'label': _('Time interval'), - 'options': [{'id': key, 'label': label} for key, label in time_interval_field.choices.items()], + 'options': [ + {'id': key, 'label': label} for key, label in time_interval_field.choices.items() + ], 'required': True, 'default': time_interval_field.default, } @@ -1195,19 +1170,17 @@ class StatisticsAPI(ViewSet): } statistics.append(data) - return Response({ - 'data': statistics, - 'err': 0, - }) + return Response( + { + 'data': statistics, + 'err': 0, + } + ) def get_statistics(self, request, klass, method): serializer = StatisticsSerializer(data=request.query_params) if not serializer.is_valid(): - response = { - 'data': [], - 'err': 1, - 'err_desc': serializer.errors - } + response = {'data': [], 'err': 1, 'err_desc': serializer.errors} return Response(response, status.HTTP_400_BAD_REQUEST) data = serializer.validated_data @@ -1231,10 +1204,12 @@ class StatisticsAPI(ViewSet): if users_ou and 'users_ou' in allowed_filters: kwargs['users_ou'] = get_object_or_404(get_ou_model(), slug=users_ou) - return Response({ - 'data': getattr(klass, method)(**kwargs), - 'err': 0, - }) + return Response( + { + 'data': getattr(klass, method)(**kwargs), + 'err': 0, + } + ) @stat(name=_('Login count by authentication type'), filters=('services_ou', 'users_ou', 'service')) def login(self, request): diff --git a/src/authentic2/app.py b/src/authentic2/app.py index a02c23b8..a076f7b5 100644 --- a/src/authentic2/app.py +++ b/src/authentic2/app.py @@ -20,11 +20,11 @@ from django.views import debug from . import plugins + class Authentic2Config(AppConfig): name = 'authentic2' verbose_name = 'Authentic2' def ready(self): plugins.init() - debug.HIDDEN_SETTINGS = re.compile( - 'API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|LDAP') + debug.HIDDEN_SETTINGS = re.compile('API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|LDAP') diff --git a/src/authentic2/app_settings.py b/src/authentic2/app_settings.py index cab4ba15..56b7c72b 100644 --- a/src/authentic2/app_settings.py +++ b/src/authentic2/app_settings.py @@ -44,6 +44,7 @@ class AppSettings(object): def settings(self): if not hasattr(self, '_settings'): from django.conf import settings + self._settings = settings return self._settings @@ -59,7 +60,9 @@ class AppSettings(object): realms[realm] = realm else: realms[realm[0]] = realm[1] + from django.contrib.auth import get_backends + for backend in get_backends(): if hasattr(backend, 'get_realms'): add_realms(backend.get_realms()) @@ -87,7 +90,9 @@ class AppSettings(object): if self.defaults[key].has_default(): return self.defaults[key].default raise ImproperlyConfigured( - 'missing setting %s(%s) is mandatory' % (key, self.defaults[key].description)) + 'missing setting %s(%s) is mandatory' % (key, self.defaults[key].description) + ) + default_settings = dict( ATTRIBUTE_BACKENDS=Setting( @@ -104,259 +109,220 @@ default_settings = dict( CAFILE=Setting( names=('AUTHENTIC2_CAFILE', 'CAFILE'), default=None, - definition='File containing certificate chains as PEM certificates'), + definition='File containing certificate chains as PEM certificates', + ), A2_REGISTRATION_CAN_DELETE_ACCOUNT=Setting( - default=True, - definition='Can user self delete their account and all their data'), + default=True, definition='Can user self delete their account and all their data' + ), A2_REGISTRATION_CAN_CHANGE_PASSWORD=Setting( - default=True, - definition='Allow user to change its own password'), + default=True, definition='Allow user to change its own password' + ), A2_REGISTRATION_EMAIL_BLACKLIST=Setting( - default=[], - definition='List of forbidden email wildcards, ex.: ^.*@ville.fr$'), + default=[], definition='List of forbidden email wildcards, ex.: ^.*@ville.fr$' + ), A2_REGISTRATION_REDIRECT=Setting( default=None, definition='Forced redirection after each redirect, NEXT_URL substring is replaced' - ' by the original next_url passed to /accounts/register/'), - A2_PROFILE_CAN_CHANGE_EMAIL=Setting( - default=True, - definition='Can user self change their email'), - A2_PROFILE_CAN_EDIT_PROFILE=Setting( - default=True, - definition='Can user self edit their profile'), - A2_PROFILE_CAN_MANAGE_FEDERATION=Setting( - default=True, - definition='Can user manage its federations'), + ' by the original next_url passed to /accounts/register/', + ), + A2_PROFILE_CAN_CHANGE_EMAIL=Setting(default=True, definition='Can user self change their email'), + A2_PROFILE_CAN_EDIT_PROFILE=Setting(default=True, definition='Can user self edit their profile'), + A2_PROFILE_CAN_MANAGE_FEDERATION=Setting(default=True, definition='Can user manage its federations'), A2_PROFILE_CAN_MANAGE_SERVICE_AUTHORIZATIONS=Setting( - default=True, - definition='Allow user to revoke granted services access to its account profile data'), - A2_PROFILE_DISPLAY_EMPTY_FIELDS=Setting( - default=False, - definition='Include empty fields in profile view'), - A2_HOMEPAGE_URL=Setting( - default=None, - definition='IdP has no homepage, redirect to this one.'), - A2_USER_CAN_RESET_PASSWORD=Setting( - default=None, - definition='Allow online reset of passwords'), + default=True, definition='Allow user to revoke granted services access to its account profile data' + ), + A2_PROFILE_DISPLAY_EMPTY_FIELDS=Setting(default=False, definition='Include empty fields in profile view'), + A2_HOMEPAGE_URL=Setting(default=None, definition='IdP has no homepage, redirect to this one.'), + A2_USER_CAN_RESET_PASSWORD=Setting(default=None, definition='Allow online reset of passwords'), A2_RESET_PASSWORD_ID_LABEL=Setting( - default=None, - definition='Alternate ID label for the password reset form'), - A2_EMAIL_IS_UNIQUE=Setting( - default=False, - definition='Email of users must be unique'), + default=None, definition='Alternate ID label for the password reset form' + ), + A2_EMAIL_IS_UNIQUE=Setting(default=False, definition='Email of users must be unique'), A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting( - default=False, - definition='Email of registered accounts must be unique'), + default=False, definition='Email of registered accounts must be unique' + ), A2_REGISTRATION_FORM_USERNAME_REGEX=Setting( - default=r'^[\w.@+-]+$', - definition='Regex to validate usernames'), + default=r'^[\w.@+-]+$', definition='Regex to validate usernames' + ), A2_REGISTRATION_FORM_USERNAME_HELP_TEXT=Setting( - default=_('Required. At most 30 characters. Letters, digits, and @/./+/-/_ only.')), - A2_REGISTRATION_FORM_USERNAME_LABEL=Setting( - default=_('Username')), + default=_('Required. At most 30 characters. Letters, digits, and @/./+/-/_ only.') + ), + A2_REGISTRATION_FORM_USERNAME_LABEL=Setting(default=_('Username')), A2_REGISTRATION_REALM=Setting( - default=None, - definition='Default realm to assign to self-registrated users'), - A2_REGISTRATION_GROUPS=Setting( - default=(), - definition='Default groups for self-registered users'), - A2_PROFILE_FIELDS=Setting( - default=(), - definition='Fields to show to the user in the profile page'), + default=None, definition='Default realm to assign to self-registrated users' + ), + A2_REGISTRATION_GROUPS=Setting(default=(), definition='Default groups for self-registered users'), + A2_PROFILE_FIELDS=Setting(default=(), definition='Fields to show to the user in the profile page'), A2_REGISTRATION_FIELDS=Setting( - default=(), - definition='Fields from the user model that must appear on the registration form'), - A2_REQUIRED_FIELDS=Setting( - default=(), - definition='User fields that are required'), + default=(), definition='Fields from the user model that must appear on the registration form' + ), + A2_REQUIRED_FIELDS=Setting(default=(), definition='User fields that are required'), A2_REGISTRATION_REQUIRED_FIELDS=Setting( - default=(), - definition='Fields from the registration form that must be required'), - A2_PRE_REGISTRATION_FIELDS=Setting( - default=(), - definition='User fields to ask with email'), - A2_REALMS=Setting( - default=(), - definition='List of realms to search user accounts'), - A2_USERNAME_REGEX=Setting( - default=None, - definition='Regex that username must validate'), - A2_USERNAME_LABEL=Setting( - default=None, - definition='Alternate username label for the login form'), + default=(), definition='Fields from the registration form that must be required' + ), + A2_PRE_REGISTRATION_FIELDS=Setting(default=(), definition='User fields to ask with email'), + A2_REALMS=Setting(default=(), definition='List of realms to search user accounts'), + A2_USERNAME_REGEX=Setting(default=None, definition='Regex that username must validate'), + A2_USERNAME_LABEL=Setting(default=None, definition='Alternate username label for the login form'), A2_USERNAME_HELP_TEXT=Setting( - default=None, - definition='Help text to explain validation rules of usernames'), - A2_USERNAME_IS_UNIQUE=Setting( - default=True, - definition='Check username uniqueness'), + default=None, definition='Help text to explain validation rules of usernames' + ), + A2_USERNAME_IS_UNIQUE=Setting(default=True, definition='Check username uniqueness'), A2_LOGIN_FORM_OU_SELECTOR=Setting( - default=False, - definition='Whether to add an OU selector to the login form'), - A2_LOGIN_FORM_OU_SELECTOR_LABEL=Setting( - default=None, - definition='Label of OU field on login page'), + default=False, definition='Whether to add an OU selector to the login form' + ), + A2_LOGIN_FORM_OU_SELECTOR_LABEL=Setting(default=None, definition='Label of OU field on login page'), A2_REGISTRATION_USERNAME_IS_UNIQUE=Setting( - default=True, - definition='Check username uniqueness on registration'), + default=True, definition='Check username uniqueness on registration' + ), IDP_BACKENDS=(), AUTH_FRONTENDS=(), AUTH_FRONTENDS_KWARGS={}, - VALID_REFERERS=Setting( - default=(), - definition='List of prefix to match referers'), - A2_OPENED_SESSION_COOKIE_NAME=Setting( - default='A2_OPENED_SESSION', - definition='Authentic session open'), - A2_OPENED_SESSION_COOKIE_DOMAIN=Setting( - default=None), - A2_OPENED_SESSION_COOKIE_SECURE=Setting( - default=False), - A2_ATTRIBUTE_KINDS=Setting( - default=(), - definition='List of other attribute kinds'), + VALID_REFERERS=Setting(default=(), definition='List of prefix to match referers'), + A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'), + A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None), + A2_OPENED_SESSION_COOKIE_SECURE=Setting(default=False), + A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'), A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting( - default=200, - definition='Width and height for a profile image'), + default=200, definition='Width and height for a profile image' + ), A2_VALIDATE_EMAIL=Setting( - default=False, - definition='Validate user email server by doing an RCPT command'), - A2_VALIDATE_EMAIL_DOMAIN=Setting( - default=True, - definition='Validate user email domain'), + default=False, definition='Validate user email server by doing an RCPT command' + ), + A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), A2_PASSWORD_POLICY_MIN_CLASSES=Setting( - default=3, - definition='Minimum number of characters classes to be present in passwords'), - A2_PASSWORD_POLICY_MIN_LENGTH=Setting( - default=8, - definition='Minimum number of characters in a password'), - A2_PASSWORD_POLICY_REGEX=Setting( - default=None, - definition='Regular expression for validating passwords'), + default=3, definition='Minimum number of characters classes to be present in passwords' + ), + A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), + A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting( default=None, - definition='Error message to show when the password do not validate the regular expression'), + definition='Error message to show when the password do not validate the regular expression', + ), A2_PASSWORD_POLICY_CLASS=Setting( default='authentic2.passwords.DefaultPasswordChecker', - definition='path of a class to validate passwords'), + definition='path of a class to validate passwords', + ), A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting( - default=False, - definition='Show last character in password fields'), + default=False, definition='Show last character in password fields' + ), A2_AUTH_PASSWORD_ENABLE=Setting( - default=True, - definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), + default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',) + ), A2_SUGGESTED_EMAIL_DOMAINS=Setting( - default=['gmail.com', 'msn.com', 'hotmail.com', 'hotmail.fr', - 'wanadoo.fr', 'yahoo.fr', 'yahoo.com', 'laposte.net', - 'free.fr', 'orange.fr', 'numericable.fr'], - definition='List of suggested email domains'), + default=[ + 'gmail.com', + 'msn.com', + 'hotmail.com', + 'hotmail.fr', + 'wanadoo.fr', + 'yahoo.fr', + 'yahoo.com', + 'laposte.net', + 'free.fr', + 'orange.fr', + 'numericable.fr', + ], + definition='List of suggested email domains', + ), A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting( default=0, definition='Failure count before logging a warning to ' 'authentic2.user_login_failure. No warning will be send if value is ' - '0.'), - PUSH_PROFILE_UPDATES=Setting( - default=False, - definition='Push profile update to linked services'), - TEMPLATE_VARS=Setting( - default={}, - definition='Variable to pass to templates'), + '0.', + ), + PUSH_PROFILE_UPDATES=Setting(default=False, definition='Push profile update to linked services'), + TEMPLATE_VARS=Setting(default={}, definition='Variable to pass to templates'), A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR=Setting( default=1.8, - definition='exponential backoff factor duration as seconds until ' - 'next try after a login failure'), + definition='exponential backoff factor duration as seconds until ' 'next try after a login failure', + ), A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION=Setting( default=1, definition='exponential backoff base factor duration as seconds ' - 'until next try after a login failure'), + 'until next try after a login failure', + ), A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION=Setting( default=3600, definition='maximum exponential backoff maximum duration as seconds until ' - 'next try after a login failure'), + 'next try after a login failure', + ), A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION=Setting( default=10, definition='minimum exponential backoff maximum duration as seconds until ' - 'next try after a login failure'), - A2_VERIFY_SSL=Setting( - default=True, - definition='Verify SSL certificate in HTTP requests'), - A2_ATTRIBUTE_KIND_TITLE_CHOICES=Setting( - default=(), - definition='Choices for the title attribute kind'), + 'next try after a login failure', + ), + A2_VERIFY_SSL=Setting(default=True, definition='Verify SSL certificate in HTTP requests'), + A2_ATTRIBUTE_KIND_TITLE_CHOICES=Setting(default=(), definition='Choices for the title attribute kind'), A2_CORS_WHITELIST=Setting( - default=(), - definition='List of origin URL to whitelist, must be scheme://netloc[:port]'), + default=(), definition='List of origin URL to whitelist, must be scheme://netloc[:port]' + ), A2_EMAIL_CHANGE_TOKEN_LIFETIME=Setting( - default=7200, - definition='Lifetime in seconds of the token sent to verify email adresses'), + default=7200, definition='Lifetime in seconds of the token sent to verify email adresses' + ), A2_DELETION_REQUEST_LIFETIME=Setting( - default=48*3600, - definition='Lifetime in seconds of the user account deletion request'), + default=48 * 3600, definition='Lifetime in seconds of the user account deletion request' + ), A2_REDIRECT_WHITELIST=Setting( - default=(), - definition='List of origins which are authorized to ask for redirection.'), + default=(), definition='List of origins which are authorized to ask for redirection.' + ), A2_API_USERS_REQUIRED_FIELDS=Setting( - default=(), - definition='List of fields to require on user\'s API, override other settings'), + default=(), definition='List of fields to require on user\'s API, override other settings' + ), A2_USER_FILTER=Setting( default={}, - definition='Filters (as in QuerySet.filter() to apply to User queryset before ' - 'authentication'), + definition='Filters (as in QuerySet.filter() to apply to User queryset before ' 'authentication', + ), A2_USER_EXCLUDE=Setting( default={}, definition='Exclusion filter (as in QuerySet.exclude() to apply to User queryset before ' - 'authentication'), + 'authentication', + ), A2_USER_REMEMBER_ME=Setting( default=None, definition='Session duration as seconds when using the remember me ' - 'checkbox. Truthiness activates the checkbox.'), + 'checkbox. Truthiness activates the checkbox.', + ), A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE=Setting( - default=False, - definition='Redirect authenticated users to homepage'), + default=False, definition='Redirect authenticated users to homepage' + ), A2_LOGIN_DISPLAY_A_CANCEL_BUTTON=Setting( default=False, - definition='Display a cancel button.' - 'This is only applicable for Liberty single sign on requests'), + definition='Display a cancel button.' 'This is only applicable for Liberty single sign on requests', + ), A2_SET_RANDOM_PASSWORD_ON_RESET=Setting( default=True, - definition='Set a random password on request to reset the password from the front-office'), - A2_ACCOUNTS_URL=Setting( - default=None, - definition='IdP has no account page, redirect to this one.'), - A2_CACHE_ENABLED=Setting( - default=True, - definition='Disable all cache decorators for testing purpose.'), - A2_ACCEPT_EMAIL_AUTHENTICATION=Setting( - default=True, - definition='Enable authentication by email'), + definition='Set a random password on request to reset the password from the front-office', + ), + A2_ACCOUNTS_URL=Setting(default=None, definition='IdP has no account page, redirect to this one.'), + A2_CACHE_ENABLED=Setting(default=True, definition='Disable all cache decorators for testing purpose.'), + A2_ACCEPT_EMAIL_AUTHENTICATION=Setting(default=True, definition='Enable authentication by email'), A2_EMAILS_IP_RATELIMIT=Setting( - default='10/h', - definition='Maximum rate of email sendings triggered by the same IP address.'), + default='10/h', definition='Maximum rate of email sendings triggered by the same IP address.' + ), A2_EMAILS_ADDRESS_RATELIMIT=Setting( - default='3/d', - definition='Maximum rate of emails sent to the same email address.'), + default='3/d', definition='Maximum rate of emails sent to the same email address.' + ), A2_USER_DELETED_KEEP_DATA=Setting( - default=['email', 'uuid'], - definition='User data to keep after deletion'), + default=['email', 'uuid'], definition='User data to keep after deletion' + ), A2_USER_DELETED_KEEP_DATA_DAYS=Setting( - default=365, - definition='Number of days to keep data on deleted users'), + default=365, definition='Number of days to keep data on deleted users' + ), A2_TOKEN_EXISTS_WARNING=Setting( - default=True, - definition='If an active token exists, warn user before generating a new one.'), + default=True, definition='If an active token exists, warn user before generating a new one.' + ), A2_DUPLICATES_THRESHOLD=Setting( - default=0.7, - definition='Trigram similarity threshold for considering user as duplicate.'), - A2_FTS_THRESHOLD=Setting( - default=0.2, - definition='Trigram similarity threshold for free text search.'), + default=0.7, definition='Trigram similarity threshold for considering user as duplicate.' + ), + A2_FTS_THRESHOLD=Setting(default=0.2, definition='Trigram similarity threshold for free text search.'), A2_DUPLICATES_BIRTHDATE_BONUS=Setting( - default=0.3, - definition='Bonus in case of birthdate match (no bonus is 0, max is 1).'), + default=0.3, definition='Bonus in case of birthdate match (no bonus is 0, max is 1).' + ), A2_EMAIL_FORMAT=Setting( default='multipart/alternative', - definition='Send email as "multiplart/alternative" or limit to "text/plain" or "text/html".'), + definition='Send email as "multiplart/alternative" or limit to "text/plain" or "text/html".', + ), ) app_settings = AppSettings(default_settings) diff --git a/src/authentic2/apps/journal/migrations/0001_initial.py b/src/authentic2/apps/journal/migrations/0001_initial.py index 2fb80e8b..7bcdc79d 100644 --- a/src/authentic2/apps/journal/migrations/0001_initial.py +++ b/src/authentic2/apps/journal/migrations/0001_initial.py @@ -113,6 +113,6 @@ class Migration(migrations.Migration): 'DROP INDEX journal_event_reference_ct_ids_idx;', 'DROP INDEX journal_event_reference_ids_idx;', 'DROP INDEX journal_event_timestamp_id_idx;', - ] + ], ), ] diff --git a/src/authentic2/apps/journal/models.py b/src/authentic2/apps/journal/models.py index 9d4b5119..9963432a 100644 --- a/src/authentic2/apps/journal/models.py +++ b/src/authentic2/apps/journal/models.py @@ -307,11 +307,15 @@ class Event(models.Model): type = models.ForeignKey(verbose_name=_('type'), to=EventType, on_delete=models.PROTECT) reference_ids = ArrayField( - verbose_name=_('reference ids'), base_field=models.BigIntegerField(), null=True, + verbose_name=_('reference ids'), + base_field=models.BigIntegerField(), + null=True, ) reference_ct_ids = ArrayField( - verbose_name=_('reference ct ids'), base_field=models.IntegerField(), null=True, + verbose_name=_('reference ct ids'), + base_field=models.IntegerField(), + null=True, ) data = JSONField(verbose_name=_('data'), null=True) @@ -361,8 +365,8 @@ class Event(models.Model): @classmethod def cleanup(cls): - '''Expire old events by default retention days or customized at the - EventTypeDefinition level.''' + """Expire old events by default retention days or customized at the + EventTypeDefinition level.""" event_types_by_retention_days = defaultdict(set) default_retention_days = getattr(settings, 'JOURNAL_DEFAULT_RETENTION_DAYS', 365 * 2) for event_type in EventType.objects.all(): diff --git a/src/authentic2/apps/journal/search_engine.py b/src/authentic2/apps/journal/search_engine.py index fa01029b..58ef2205 100644 --- a/src/authentic2/apps/journal/search_engine.py +++ b/src/authentic2/apps/journal/search_engine.py @@ -80,12 +80,14 @@ class SearchEngine: if not hasattr(self, method_name): return - yield from getattr(self, method_name)(lexem[len(prefix) + 1:]) + yield from getattr(self, method_name)(lexem[len(prefix) + 1 :]) @classmethod def documentation(cls): - yield _('You can use colon terminated prefixes to make special searches, ' - 'and you can use quote around the suffix to preserve spaces.') + yield _( + 'You can use colon terminated prefixes to make special searches, ' + 'and you can use quote around the suffix to preserve spaces.' + ) for name in dir(cls): documentation = getattr(getattr(cls, name), 'documentation', None) if documentation: diff --git a/src/authentic2/attribute_aggregator/migrations/0001_initial.py b/src/authentic2/attribute_aggregator/migrations/0001_initial.py index aa178826..9ce675ea 100644 --- a/src/authentic2/attribute_aggregator/migrations/0001_initial.py +++ b/src/authentic2/attribute_aggregator/migrations/0001_initial.py @@ -15,10 +15,306 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AttributeItem', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('attribute_name', models.CharField(default=('OpenLDAProotDSE', 'OpenLDAProotDSE'), max_length=100, verbose_name='Attribute name', choices=[('OpenLDAProotDSE', 'OpenLDAProotDSE'), ('aRecord', 'aRecord'), ('administrativeRole', 'administrativeRole'), ('alias', 'alias'), ('aliasedObjectName', 'aliasedObjectName'), ('altServer', 'altServer'), ('associatedDomain', 'associatedDomain'), ('associatedName', 'associatedName'), ('attributeTypes', 'attributeTypes'), ('audio', 'audio'), ('authPassword', 'authPassword'), ('authorityRevocationList', 'authorityRevocationList'), ('authzFrom', 'authzFrom'), ('authzTo', 'authzTo'), ('bootFile', 'bootFile'), ('bootParameter', 'bootParameter'), ('buildingName', 'buildingName'), ('businessCategory', 'businessCategory'), ('c', 'c'), ('cACertificate', 'cACertificate'), ('cNAMERecord', 'cNAMERecord'), ('carLicense', 'carLicense'), ('certificateRevocationList', 'certificateRevocationList'), ('children', 'children'), ('cn', 'cn'), ('co', 'co'), ('collectiveAttributeSubentries', 'collectiveAttributeSubentries'), ('collectiveAttributeSubentry', 'collectiveAttributeSubentry'), ('collectiveExclusions', 'collectiveExclusions'), ('configContext', 'configContext'), ('contextCSN', 'contextCSN'), ('createTimestamp', 'createTimestamp'), ('creatorsName', 'creatorsName'), ('crossCertificatePair', 'crossCertificatePair'), ('dITContentRules', 'dITContentRules'), ('dITRedirect', 'dITRedirect'), ('dITStructureRules', 'dITStructureRules'), ('dSAQuality', 'dSAQuality'), ('dc', 'dc'), ('deltaRevocationList', 'deltaRevocationList'), ('departmentNumber', 'departmentNumber'), ('description', 'description'), ('destinationIndicator', 'destinationIndicator'), ('displayName', 'displayName'), ('distinguishedName', 'distinguishedName'), ('dmdName', 'dmdName'), ('dnQualifier', 'dnQualifier'), ('documentAuthor', 'documentAuthor'), ('documentIdentifier', 'documentIdentifier'), ('documentLocation', 'documentLocation'), ('documentPublisher', 'documentPublisher'), ('documentTitle', 'documentTitle'), ('documentVersion', 'documentVersion'), ('drink', 'drink'), ('dynamicObject', 'dynamicObject'), ('dynamicSubtrees', 'dynamicSubtrees'), ('eduOrgHomePageURI', 'eduOrgHomePageURI'), ('eduOrgIdentityAuthNPolicyURI', 'eduOrgIdentityAuthNPolicyURI'), ('eduOrgLegalName', 'eduOrgLegalName'), ('eduOrgSuperiorURI', 'eduOrgSuperiorURI'), ('eduOrgWhitePagesURI', 'eduOrgWhitePagesURI'), ('eduPersonAffiliation', 'eduPersonAffiliation'), ('eduPersonAssurance', 'eduPersonAssurance'), ('eduPersonEntitlement', 'eduPersonEntitlement'), ('eduPersonNickname', 'eduPersonNickname'), ('eduPersonOrgDN', 'eduPersonOrgDN'), ('eduPersonOrgUnitDN', 'eduPersonOrgUnitDN'), ('eduPersonPrimaryAffiliation', 'eduPersonPrimaryAffiliation'), ('eduPersonPrimaryOrgUnitDN', 'eduPersonPrimaryOrgUnitDN'), ('eduPersonPrincipalName', 'eduPersonPrincipalName'), ('eduPersonScopedAffiliation', 'eduPersonScopedAffiliation'), ('eduPersonTargetedID', 'eduPersonTargetedID'), ('email', 'email'), ('employeeNumber', 'employeeNumber'), ('employeeType', 'employeeType'), ('enhancedSearchGuide', 'enhancedSearchGuide'), ('entry', 'entry'), ('entryCSN', 'entryCSN'), ('entryDN', 'entryDN'), ('entryTtl', 'entryTtl'), ('entryUUID', 'entryUUID'), ('extensibleObject', 'extensibleObject'), ('fax', 'fax'), ('gecos', 'gecos'), ('generationQualifier', 'generationQualifier'), ('gidNumber', 'gidNumber'), ('givenName', 'givenName'), ('glue', 'glue'), ('hasSubordinates', 'hasSubordinates'), ('homeDirectory', 'homeDirectory'), ('homePhone', 'homePhone'), ('homePostalAddress', 'homePostalAddress'), ('host', 'host'), ('houseIdentifier', 'houseIdentifier'), ('info', 'info'), ('initials', 'initials'), ('internationaliSDNNumber', 'internationaliSDNNumber'), ('ipHostNumber', 'ipHostNumber'), ('ipNetmaskNumber', 'ipNetmaskNumber'), ('ipNetworkNumber', 'ipNetworkNumber'), ('ipProtocolNumber', 'ipProtocolNumber'), ('ipServicePort', 'ipServicePort'), ('ipServiceProtocolSUPname', 'ipServiceProtocolSUPname'), ('janetMailbox', 'janetMailbox'), ('jpegPhoto', 'jpegPhoto'), ('knowledgeInformation', 'knowledgeInformation'), ('l', 'l'), ('labeledURI', 'labeledURI'), ('ldapSyntaxes', 'ldapSyntaxes'), ('loginShell', 'loginShell'), ('mDRecord', 'mDRecord'), ('mXRecord', 'mXRecord'), ('macAddress', 'macAddress'), ('mail', 'mail'), ('mailForwardingAddress', 'mailForwardingAddress'), ('mailHost', 'mailHost'), ('mailLocalAddress', 'mailLocalAddress'), ('mailPreferenceOption', 'mailPreferenceOption'), ('mailRoutingAddress', 'mailRoutingAddress'), ('manager', 'manager'), ('matchingRuleUse', 'matchingRuleUse'), ('matchingRules', 'matchingRules'), ('member', 'member'), ('memberNisNetgroup', 'memberNisNetgroup'), ('memberUid', 'memberUid'), ('mobile', 'mobile'), ('modifiersName', 'modifiersName'), ('modifyTimestamp', 'modifyTimestamp'), ('monitorContext', 'monitorContext'), ('nSRecord', 'nSRecord'), ('name', 'name'), ('nameForms', 'nameForms'), ('namingCSN', 'namingCSN'), ('namingContexts', 'namingContexts'), ('nisMapEntry', 'nisMapEntry'), ('nisMapNameSUPname', 'nisMapNameSUPname'), ('nisNetgroupTriple', 'nisNetgroupTriple'), ('o', 'o'), ('objectClass', 'objectClass'), ('objectClasses', 'objectClasses'), ('oncRpcNumber', 'oncRpcNumber'), ('organizationalStatus', 'organizationalStatus'), ('otherMailbox', 'otherMailbox'), ('ou', 'ou'), ('owner', 'owner'), ('pager', 'pager'), ('personalSignature', 'personalSignature'), ('personalTitle', 'personalTitle'), ('photo', 'photo'), ('physicalDeliveryOfficeName', 'physicalDeliveryOfficeName'), ('postOfficeBox', 'postOfficeBox'), ('postalAddress', 'postalAddress'), ('postalCode', 'postalCode'), ('preferredDeliveryMethod', 'preferredDeliveryMethod'), ('preferredLanguage', 'preferredLanguage'), ('presentationAddress', 'presentationAddress'), ('protocolInformation', 'protocolInformation'), ('pseudonym', 'pseudonym'), ('ref', 'ref'), ('referral', 'referral'), ('registeredAddress', 'registeredAddress'), ('rfc822MailMember', 'rfc822MailMember'), ('role', 'role'), ('roleOccupant', 'roleOccupant'), ('roomNumber', 'roomNumber'), ('sOARecord', 'sOARecord'), ('schacHomeOrganization', 'schacHomeOrganization'), ('schacHomeOrganizationType', 'schacHomeOrganizationType'), ('searchGuide', 'searchGuide'), ('secretary', 'secretary'), ('seeAlso', 'seeAlso'), ('serialNumber', 'serialNumber'), ('shadowExpire', 'shadowExpire'), ('shadowFlag', 'shadowFlag'), ('shadowInactive', 'shadowInactive'), ('shadowLastChange', 'shadowLastChange'), ('shadowMax', 'shadowMax'), ('shadowMin', 'shadowMin'), ('shadowWarning', 'shadowWarning'), ('singleLevelQuality', 'singleLevelQuality'), ('sn', 'sn'), ('st', 'st'), ('street', 'street'), ('structuralObjectClass', 'structuralObjectClass'), ('subentry', 'subentry'), ('subschema', 'subschema'), ('subschemaSubentry', 'subschemaSubentry'), ('subtreeMaximumQuality', 'subtreeMaximumQuality'), ('subtreeMinimumQuality', 'subtreeMinimumQuality'), ('subtreeSpecification', 'subtreeSpecification'), ('supannActivite', 'supannActivite'), ('supannAffectation', 'supannAffectation'), ('supannAliasLogin', 'supannAliasLogin'), ('supannAutreMail', 'supannAutreMail'), ('supannAutreTelephone', 'supannAutreTelephone'), ('supannCivilite', 'supannCivilite'), ('supannCodeEntite', 'supannCodeEntite'), ('supannCodeEntiteParent', 'supannCodeEntiteParent'), ('supannCodeINE', 'supannCodeINE'), ('supannEmpCorps', 'supannEmpCorps'), ('supannEmpId', 'supannEmpId'), ('supannEntiteAffectation', 'supannEntiteAffectation'), ('supannEntiteAffectationPrincipale', 'supannEntiteAffectationPrincipale'), ('supannEtablissement', 'supannEtablissement'), ('supannEtuAnneeInscription', 'supannEtuAnneeInscription'), ('supannEtuCursusAnnee', 'supannEtuCursusAnnee'), ('supannEtuDiplome', 'supannEtuDiplome'), ('supannEtuElementPedagogique', 'supannEtuElementPedagogique'), ('supannEtuEtape', 'supannEtuEtape'), ('supannEtuId', 'supannEtuId'), ('supannEtuInscription', 'supannEtuInscription'), ('supannEtuRegimeInscription', 'supannEtuRegimeInscription'), ('supannEtuSecteurDisciplinaire', 'supannEtuSecteurDisciplinaire'), ('supannEtuTypeDiplome', 'supannEtuTypeDiplome'), ('supannGroupeAdminDN', 'supannGroupeAdminDN'), ('supannGroupeDateFin', 'supannGroupeDateFin'), ('supannGroupeLecteurDN', 'supannGroupeLecteurDN'), ('supannListeRouge', 'supannListeRouge'), ('supannMailPerso', 'supannMailPerso'), ('supannOrganisme', 'supannOrganisme'), ('supannParrainDN', 'supannParrainDN'), ('supannRefId', 'supannRefId'), ('supannRole', 'supannRole'), ('supannRoleEntite', 'supannRoleEntite'), ('supannRoleGenerique', 'supannRoleGenerique'), ('supannTypeEntite', 'supannTypeEntite'), ('supannTypeEntiteAffectation', 'supannTypeEntiteAffectation'), ('superiorUUID', 'superiorUUID'), ('supportedAlgorithms', 'supportedAlgorithms'), ('supportedApplicationContext', 'supportedApplicationContext'), ('supportedAuthPasswordSchemes', 'supportedAuthPasswordSchemes'), ('supportedControl', 'supportedControl'), ('supportedExtension', 'supportedExtension'), ('supportedFeatures', 'supportedFeatures'), ('supportedLDAPVersion', 'supportedLDAPVersion'), ('supportedSASLMechanisms', 'supportedSASLMechanisms'), ('syncConsumerSubentry', 'syncConsumerSubentry'), ('syncProviderSubentry', 'syncProviderSubentry'), ('syncTimestamp', 'syncTimestamp'), ('syncreplCookie', 'syncreplCookie'), ('telephoneNumber', 'telephoneNumber'), ('teletexTerminalIdentifier', 'teletexTerminalIdentifier'), ('telexNumber', 'telexNumber'), ('textEncodedORAddress', 'textEncodedORAddress'), ('title', 'title'), ('top', 'top'), ('uid', 'uid'), ('uidNumber', 'uidNumber'), ('uniqueIdentifier', 'uniqueIdentifier'), ('uniqueMember', 'uniqueMember'), ('userCertificate', 'userCertificate'), ('userClass', 'userClass'), ('userPKCS12', 'userPKCS12'), ('userPassword', 'userPassword'), ('userSMIMECertificate', 'userSMIMECertificate'), ('vendorName', 'vendorName'), ('vendorVersion', 'vendorVersion'), ('x121Address', 'x121Address'), ('x500UniqueIdentifier', 'x500UniqueIdentifier')])), - ('output_name_format', models.CharField(default=('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'SAMLv2 URI'), max_length=100, verbose_name='Output name format', choices=[('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'SAMLv2 URI'), ('urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'SAMLv2 BASIC')])), - ('output_namespace', models.CharField(default=('Default', 'Default'), max_length=100, verbose_name='Output namespace', choices=[('Default', 'Default'), ('http://schemas.xmlsoap.org/ws/2005/05/identity/claims', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims')])), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), + ( + 'attribute_name', + models.CharField( + default=('OpenLDAProotDSE', 'OpenLDAProotDSE'), + max_length=100, + verbose_name='Attribute name', + choices=[ + ('OpenLDAProotDSE', 'OpenLDAProotDSE'), + ('aRecord', 'aRecord'), + ('administrativeRole', 'administrativeRole'), + ('alias', 'alias'), + ('aliasedObjectName', 'aliasedObjectName'), + ('altServer', 'altServer'), + ('associatedDomain', 'associatedDomain'), + ('associatedName', 'associatedName'), + ('attributeTypes', 'attributeTypes'), + ('audio', 'audio'), + ('authPassword', 'authPassword'), + ('authorityRevocationList', 'authorityRevocationList'), + ('authzFrom', 'authzFrom'), + ('authzTo', 'authzTo'), + ('bootFile', 'bootFile'), + ('bootParameter', 'bootParameter'), + ('buildingName', 'buildingName'), + ('businessCategory', 'businessCategory'), + ('c', 'c'), + ('cACertificate', 'cACertificate'), + ('cNAMERecord', 'cNAMERecord'), + ('carLicense', 'carLicense'), + ('certificateRevocationList', 'certificateRevocationList'), + ('children', 'children'), + ('cn', 'cn'), + ('co', 'co'), + ('collectiveAttributeSubentries', 'collectiveAttributeSubentries'), + ('collectiveAttributeSubentry', 'collectiveAttributeSubentry'), + ('collectiveExclusions', 'collectiveExclusions'), + ('configContext', 'configContext'), + ('contextCSN', 'contextCSN'), + ('createTimestamp', 'createTimestamp'), + ('creatorsName', 'creatorsName'), + ('crossCertificatePair', 'crossCertificatePair'), + ('dITContentRules', 'dITContentRules'), + ('dITRedirect', 'dITRedirect'), + ('dITStructureRules', 'dITStructureRules'), + ('dSAQuality', 'dSAQuality'), + ('dc', 'dc'), + ('deltaRevocationList', 'deltaRevocationList'), + ('departmentNumber', 'departmentNumber'), + ('description', 'description'), + ('destinationIndicator', 'destinationIndicator'), + ('displayName', 'displayName'), + ('distinguishedName', 'distinguishedName'), + ('dmdName', 'dmdName'), + ('dnQualifier', 'dnQualifier'), + ('documentAuthor', 'documentAuthor'), + ('documentIdentifier', 'documentIdentifier'), + ('documentLocation', 'documentLocation'), + ('documentPublisher', 'documentPublisher'), + ('documentTitle', 'documentTitle'), + ('documentVersion', 'documentVersion'), + ('drink', 'drink'), + ('dynamicObject', 'dynamicObject'), + ('dynamicSubtrees', 'dynamicSubtrees'), + ('eduOrgHomePageURI', 'eduOrgHomePageURI'), + ('eduOrgIdentityAuthNPolicyURI', 'eduOrgIdentityAuthNPolicyURI'), + ('eduOrgLegalName', 'eduOrgLegalName'), + ('eduOrgSuperiorURI', 'eduOrgSuperiorURI'), + ('eduOrgWhitePagesURI', 'eduOrgWhitePagesURI'), + ('eduPersonAffiliation', 'eduPersonAffiliation'), + ('eduPersonAssurance', 'eduPersonAssurance'), + ('eduPersonEntitlement', 'eduPersonEntitlement'), + ('eduPersonNickname', 'eduPersonNickname'), + ('eduPersonOrgDN', 'eduPersonOrgDN'), + ('eduPersonOrgUnitDN', 'eduPersonOrgUnitDN'), + ('eduPersonPrimaryAffiliation', 'eduPersonPrimaryAffiliation'), + ('eduPersonPrimaryOrgUnitDN', 'eduPersonPrimaryOrgUnitDN'), + ('eduPersonPrincipalName', 'eduPersonPrincipalName'), + ('eduPersonScopedAffiliation', 'eduPersonScopedAffiliation'), + ('eduPersonTargetedID', 'eduPersonTargetedID'), + ('email', 'email'), + ('employeeNumber', 'employeeNumber'), + ('employeeType', 'employeeType'), + ('enhancedSearchGuide', 'enhancedSearchGuide'), + ('entry', 'entry'), + ('entryCSN', 'entryCSN'), + ('entryDN', 'entryDN'), + ('entryTtl', 'entryTtl'), + ('entryUUID', 'entryUUID'), + ('extensibleObject', 'extensibleObject'), + ('fax', 'fax'), + ('gecos', 'gecos'), + ('generationQualifier', 'generationQualifier'), + ('gidNumber', 'gidNumber'), + ('givenName', 'givenName'), + ('glue', 'glue'), + ('hasSubordinates', 'hasSubordinates'), + ('homeDirectory', 'homeDirectory'), + ('homePhone', 'homePhone'), + ('homePostalAddress', 'homePostalAddress'), + ('host', 'host'), + ('houseIdentifier', 'houseIdentifier'), + ('info', 'info'), + ('initials', 'initials'), + ('internationaliSDNNumber', 'internationaliSDNNumber'), + ('ipHostNumber', 'ipHostNumber'), + ('ipNetmaskNumber', 'ipNetmaskNumber'), + ('ipNetworkNumber', 'ipNetworkNumber'), + ('ipProtocolNumber', 'ipProtocolNumber'), + ('ipServicePort', 'ipServicePort'), + ('ipServiceProtocolSUPname', 'ipServiceProtocolSUPname'), + ('janetMailbox', 'janetMailbox'), + ('jpegPhoto', 'jpegPhoto'), + ('knowledgeInformation', 'knowledgeInformation'), + ('l', 'l'), + ('labeledURI', 'labeledURI'), + ('ldapSyntaxes', 'ldapSyntaxes'), + ('loginShell', 'loginShell'), + ('mDRecord', 'mDRecord'), + ('mXRecord', 'mXRecord'), + ('macAddress', 'macAddress'), + ('mail', 'mail'), + ('mailForwardingAddress', 'mailForwardingAddress'), + ('mailHost', 'mailHost'), + ('mailLocalAddress', 'mailLocalAddress'), + ('mailPreferenceOption', 'mailPreferenceOption'), + ('mailRoutingAddress', 'mailRoutingAddress'), + ('manager', 'manager'), + ('matchingRuleUse', 'matchingRuleUse'), + ('matchingRules', 'matchingRules'), + ('member', 'member'), + ('memberNisNetgroup', 'memberNisNetgroup'), + ('memberUid', 'memberUid'), + ('mobile', 'mobile'), + ('modifiersName', 'modifiersName'), + ('modifyTimestamp', 'modifyTimestamp'), + ('monitorContext', 'monitorContext'), + ('nSRecord', 'nSRecord'), + ('name', 'name'), + ('nameForms', 'nameForms'), + ('namingCSN', 'namingCSN'), + ('namingContexts', 'namingContexts'), + ('nisMapEntry', 'nisMapEntry'), + ('nisMapNameSUPname', 'nisMapNameSUPname'), + ('nisNetgroupTriple', 'nisNetgroupTriple'), + ('o', 'o'), + ('objectClass', 'objectClass'), + ('objectClasses', 'objectClasses'), + ('oncRpcNumber', 'oncRpcNumber'), + ('organizationalStatus', 'organizationalStatus'), + ('otherMailbox', 'otherMailbox'), + ('ou', 'ou'), + ('owner', 'owner'), + ('pager', 'pager'), + ('personalSignature', 'personalSignature'), + ('personalTitle', 'personalTitle'), + ('photo', 'photo'), + ('physicalDeliveryOfficeName', 'physicalDeliveryOfficeName'), + ('postOfficeBox', 'postOfficeBox'), + ('postalAddress', 'postalAddress'), + ('postalCode', 'postalCode'), + ('preferredDeliveryMethod', 'preferredDeliveryMethod'), + ('preferredLanguage', 'preferredLanguage'), + ('presentationAddress', 'presentationAddress'), + ('protocolInformation', 'protocolInformation'), + ('pseudonym', 'pseudonym'), + ('ref', 'ref'), + ('referral', 'referral'), + ('registeredAddress', 'registeredAddress'), + ('rfc822MailMember', 'rfc822MailMember'), + ('role', 'role'), + ('roleOccupant', 'roleOccupant'), + ('roomNumber', 'roomNumber'), + ('sOARecord', 'sOARecord'), + ('schacHomeOrganization', 'schacHomeOrganization'), + ('schacHomeOrganizationType', 'schacHomeOrganizationType'), + ('searchGuide', 'searchGuide'), + ('secretary', 'secretary'), + ('seeAlso', 'seeAlso'), + ('serialNumber', 'serialNumber'), + ('shadowExpire', 'shadowExpire'), + ('shadowFlag', 'shadowFlag'), + ('shadowInactive', 'shadowInactive'), + ('shadowLastChange', 'shadowLastChange'), + ('shadowMax', 'shadowMax'), + ('shadowMin', 'shadowMin'), + ('shadowWarning', 'shadowWarning'), + ('singleLevelQuality', 'singleLevelQuality'), + ('sn', 'sn'), + ('st', 'st'), + ('street', 'street'), + ('structuralObjectClass', 'structuralObjectClass'), + ('subentry', 'subentry'), + ('subschema', 'subschema'), + ('subschemaSubentry', 'subschemaSubentry'), + ('subtreeMaximumQuality', 'subtreeMaximumQuality'), + ('subtreeMinimumQuality', 'subtreeMinimumQuality'), + ('subtreeSpecification', 'subtreeSpecification'), + ('supannActivite', 'supannActivite'), + ('supannAffectation', 'supannAffectation'), + ('supannAliasLogin', 'supannAliasLogin'), + ('supannAutreMail', 'supannAutreMail'), + ('supannAutreTelephone', 'supannAutreTelephone'), + ('supannCivilite', 'supannCivilite'), + ('supannCodeEntite', 'supannCodeEntite'), + ('supannCodeEntiteParent', 'supannCodeEntiteParent'), + ('supannCodeINE', 'supannCodeINE'), + ('supannEmpCorps', 'supannEmpCorps'), + ('supannEmpId', 'supannEmpId'), + ('supannEntiteAffectation', 'supannEntiteAffectation'), + ('supannEntiteAffectationPrincipale', 'supannEntiteAffectationPrincipale'), + ('supannEtablissement', 'supannEtablissement'), + ('supannEtuAnneeInscription', 'supannEtuAnneeInscription'), + ('supannEtuCursusAnnee', 'supannEtuCursusAnnee'), + ('supannEtuDiplome', 'supannEtuDiplome'), + ('supannEtuElementPedagogique', 'supannEtuElementPedagogique'), + ('supannEtuEtape', 'supannEtuEtape'), + ('supannEtuId', 'supannEtuId'), + ('supannEtuInscription', 'supannEtuInscription'), + ('supannEtuRegimeInscription', 'supannEtuRegimeInscription'), + ('supannEtuSecteurDisciplinaire', 'supannEtuSecteurDisciplinaire'), + ('supannEtuTypeDiplome', 'supannEtuTypeDiplome'), + ('supannGroupeAdminDN', 'supannGroupeAdminDN'), + ('supannGroupeDateFin', 'supannGroupeDateFin'), + ('supannGroupeLecteurDN', 'supannGroupeLecteurDN'), + ('supannListeRouge', 'supannListeRouge'), + ('supannMailPerso', 'supannMailPerso'), + ('supannOrganisme', 'supannOrganisme'), + ('supannParrainDN', 'supannParrainDN'), + ('supannRefId', 'supannRefId'), + ('supannRole', 'supannRole'), + ('supannRoleEntite', 'supannRoleEntite'), + ('supannRoleGenerique', 'supannRoleGenerique'), + ('supannTypeEntite', 'supannTypeEntite'), + ('supannTypeEntiteAffectation', 'supannTypeEntiteAffectation'), + ('superiorUUID', 'superiorUUID'), + ('supportedAlgorithms', 'supportedAlgorithms'), + ('supportedApplicationContext', 'supportedApplicationContext'), + ('supportedAuthPasswordSchemes', 'supportedAuthPasswordSchemes'), + ('supportedControl', 'supportedControl'), + ('supportedExtension', 'supportedExtension'), + ('supportedFeatures', 'supportedFeatures'), + ('supportedLDAPVersion', 'supportedLDAPVersion'), + ('supportedSASLMechanisms', 'supportedSASLMechanisms'), + ('syncConsumerSubentry', 'syncConsumerSubentry'), + ('syncProviderSubentry', 'syncProviderSubentry'), + ('syncTimestamp', 'syncTimestamp'), + ('syncreplCookie', 'syncreplCookie'), + ('telephoneNumber', 'telephoneNumber'), + ('teletexTerminalIdentifier', 'teletexTerminalIdentifier'), + ('telexNumber', 'telexNumber'), + ('textEncodedORAddress', 'textEncodedORAddress'), + ('title', 'title'), + ('top', 'top'), + ('uid', 'uid'), + ('uidNumber', 'uidNumber'), + ('uniqueIdentifier', 'uniqueIdentifier'), + ('uniqueMember', 'uniqueMember'), + ('userCertificate', 'userCertificate'), + ('userClass', 'userClass'), + ('userPKCS12', 'userPKCS12'), + ('userPassword', 'userPassword'), + ('userSMIMECertificate', 'userSMIMECertificate'), + ('vendorName', 'vendorName'), + ('vendorVersion', 'vendorVersion'), + ('x121Address', 'x121Address'), + ('x500UniqueIdentifier', 'x500UniqueIdentifier'), + ], + ), + ), + ( + 'output_name_format', + models.CharField( + default=('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'SAMLv2 URI'), + max_length=100, + verbose_name='Output name format', + choices=[ + ('urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'SAMLv2 URI'), + ('urn:oasis:names:tc:SAML:2.0:attrname-format:basic', 'SAMLv2 BASIC'), + ], + ), + ), + ( + 'output_namespace', + models.CharField( + default=('Default', 'Default'), + max_length=100, + verbose_name='Output namespace', + choices=[ + ('Default', 'Default'), + ( + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims', + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims', + ), + ], + ), + ), ('required', models.BooleanField(default=False, verbose_name='Required')), ], options={ @@ -30,9 +326,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AttributeList', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(unique=True, max_length=100, verbose_name='Name')), - ('attributes', models.ManyToManyField(related_name='attributes of the list', null=True, verbose_name='Attributes', to='attribute_aggregator.AttributeItem', blank=True)), + ( + 'attributes', + models.ManyToManyField( + related_name='attributes of the list', + null=True, + verbose_name='Attributes', + to='attribute_aggregator.AttributeItem', + blank=True, + ), + ), ], options={ 'verbose_name': 'attribute list', @@ -43,9 +351,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AttributeSource', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(unique=True, max_length=200, verbose_name='Name')), - ('namespace', models.CharField(default=('Default', 'Default'), max_length=100, verbose_name='Namespace', choices=[('Default', 'Default'), ('http://schemas.xmlsoap.org/ws/2005/05/identity/claims', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims')])), + ( + 'namespace', + models.CharField( + default=('Default', 'Default'), + max_length=100, + verbose_name='Namespace', + choices=[ + ('Default', 'Default'), + ( + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims', + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims', + ), + ], + ), + ), ], options={ 'verbose_name': 'attribute source', @@ -56,15 +381,31 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LdapSource', fields=[ - ('attributesource_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='attribute_aggregator.AttributeSource', on_delete=models.CASCADE)), + ( + 'attributesource_ptr', + models.OneToOneField( + parent_link=True, + auto_created=True, + primary_key=True, + serialize=False, + to='attribute_aggregator.AttributeSource', + on_delete=models.CASCADE, + ), + ), ('server', models.CharField(unique=True, max_length=200, verbose_name='Server')), ('user', models.CharField(max_length=200, null=True, verbose_name='User', blank=True)), - ('password', models.CharField(max_length=200, null=True, verbose_name='Password', blank=True)), + ( + 'password', + models.CharField(max_length=200, null=True, verbose_name='Password', blank=True), + ), ('base', models.CharField(max_length=200, verbose_name='Base')), ('port', models.IntegerField(default=389, verbose_name='Port')), ('ldaps', models.BooleanField(default=False, verbose_name='LDAPS')), ('certificate', models.TextField(verbose_name='Certificate', blank=True)), - ('is_auth_backend', models.BooleanField(default=False, verbose_name='Is it used for authentication?')), + ( + 'is_auth_backend', + models.BooleanField(default=False, verbose_name='Is it used for authentication?'), + ), ], options={ 'verbose_name': 'ldap attribute source', @@ -75,10 +416,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserAliasInSource', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(max_length=200, verbose_name='Name')), - ('source', models.ForeignKey(verbose_name='attribute source', to='attribute_aggregator.AttributeSource', on_delete=models.CASCADE)), - ('user', models.ForeignKey(related_name='user_alias_in_source', verbose_name='user', to='auth.User', on_delete=models.CASCADE)), + ( + 'source', + models.ForeignKey( + verbose_name='attribute source', + to='attribute_aggregator.AttributeSource', + on_delete=models.CASCADE, + ), + ), + ( + 'user', + models.ForeignKey( + related_name='user_alias_in_source', + verbose_name='user', + to='auth.User', + on_delete=models.CASCADE, + ), + ), ], options={ 'verbose_name': 'user alias from source', @@ -89,9 +448,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserAttributeProfile', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('data', models.TextField(null=True, blank=True)), - ('user', models.OneToOneField(related_name='user_attribute_profile', null=True, blank=True, to='auth.User', on_delete=models.CASCADE)), + ( + 'user', + models.OneToOneField( + related_name='user_attribute_profile', + null=True, + blank=True, + to='auth.User', + on_delete=models.CASCADE, + ), + ), ], options={ 'verbose_name': 'user attribute profile', @@ -106,7 +477,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='attributeitem', name='source', - field=models.ForeignKey(verbose_name='Attribute source', blank=True, to='attribute_aggregator.AttributeSource', null=True, on_delete=models.CASCADE), + field=models.ForeignKey( + verbose_name='Attribute source', + blank=True, + to='attribute_aggregator.AttributeSource', + null=True, + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/src/authentic2/attribute_aggregator/migrations/0002_auto_20150409_1840.py b/src/authentic2/attribute_aggregator/migrations/0002_auto_20150409_1840.py index 6d38d0b8..23618254 100644 --- a/src/authentic2/attribute_aggregator/migrations/0002_auto_20150409_1840.py +++ b/src/authentic2/attribute_aggregator/migrations/0002_auto_20150409_1840.py @@ -16,13 +16,24 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='useraliasinsource', name='user', - field=models.ForeignKey(related_name='user_alias_in_source', verbose_name='user', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + field=models.ForeignKey( + related_name='user_alias_in_source', + verbose_name='user', + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AlterField( model_name='userattributeprofile', name='user', - field=models.OneToOneField(related_name='user_attribute_profile', null=True, blank=True, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + field=models.OneToOneField( + related_name='user_attribute_profile', + null=True, + blank=True, + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/src/authentic2/attribute_aggregator/migrations/0003_auto_20150526_2239.py b/src/authentic2/attribute_aggregator/migrations/0003_auto_20150526_2239.py index ef35097c..2d33df35 100644 --- a/src/authentic2/attribute_aggregator/migrations/0003_auto_20150526_2239.py +++ b/src/authentic2/attribute_aggregator/migrations/0003_auto_20150526_2239.py @@ -15,11 +15,15 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='attributelist', name='attributes', - field=models.ManyToManyField(to='attribute_aggregator.AttributeItem', verbose_name='Attributes', blank=True), + field=models.ManyToManyField( + to='attribute_aggregator.AttributeItem', verbose_name='Attributes', blank=True + ), ), migrations.AlterField( model_name='useraliasinsource', name='user', - field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + field=models.ForeignKey( + verbose_name='user', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), ] diff --git a/src/authentic2/attribute_kinds.py b/src/authentic2/attribute_kinds.py index 939b0916..920312f4 100644 --- a/src/authentic2/attribute_kinds.py +++ b/src/authentic2/attribute_kinds.py @@ -49,6 +49,7 @@ from .forms import widgets, fields def capfirst(value): return value and value[0].upper() + value[1:] + DEFAULT_TITLE_CHOICES = ( (pgettext_lazy('title', 'Mrs'), pgettext_lazy('title', 'Mrs')), (pgettext_lazy('title', 'Mr'), pgettext_lazy('title', 'Mr')), @@ -140,9 +141,10 @@ class AddressAutocompleteField(forms.CharField): def get_title_choices(): return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES + validate_phone_number = RegexValidator( - r'^\+?\d{,20}$', - message=_('Phone number can start with a + and must contain only digits.')) + r'^\+?\d{,20}$', message=_('Phone number can start with a + and must contain only digits.') +) def clean_number(number): @@ -172,9 +174,7 @@ class PhoneNumberDRFField(serializers.CharField): return clean_number(super().to_internal_value(data)) -validate_fr_postcode = RegexValidator( - r'^\d{5}$', - message=_('The value must be a valid french postcode')) +validate_fr_postcode = RegexValidator(r'^\d{5}$', message=_('The value must be a valid french postcode')) class FrPostcodeField(forms.CharField): @@ -215,9 +215,7 @@ def profile_image_serialize(uploadedfile): for chunk in uploadedfile.chunks(): h_computation.update(chunk) hexdigest = h_computation.hexdigest() - stored_file = default_storage.save( - os.path.join('profile-image', hexdigest + '.jpeg'), - uploadedfile) + stored_file = default_storage.save(os.path.join('profile-image', hexdigest + '.jpeg'), uploadedfile) return stored_file @@ -229,8 +227,7 @@ def profile_image_deserialize(name): def profile_image_html_value(attribute, value): if value: - fragment = u'' % ( - value.url, attribute.name, value.url) + fragment = u'' % (value.url, attribute.name, value.url) return html.mark_safe(fragment) return '' @@ -276,7 +273,7 @@ DEFAULT_ATTRIBUTE_KINDS = [ 'kwargs': { 'choices': get_title_choices(), 'widget': forms.RadioSelect, - } + }, }, { 'label': _('boolean'), diff --git a/src/authentic2/attributes_ng/engine.py b/src/authentic2/attributes_ng/engine.py index 09117d2f..c7f77545 100644 --- a/src/authentic2/attributes_ng/engine.py +++ b/src/authentic2/attributes_ng/engine.py @@ -25,11 +25,12 @@ __ALL__ = ['get_attribute_names', 'get_attributes', 'get_service_attributes'] class UnsortableError(Exception): - ''' + """ Raise when topological_sort is unable to sort instance topologically. sorted_list contains the instances that could be sorted unsorted contains the instances that couldn't. - ''' + """ + def __init__(self, sorted_list, unsortable_instances): self.sorted_list = sorted_list self.unsortable_instances = unsortable_instances @@ -39,9 +40,9 @@ class UnsortableError(Exception): def topological_sort(source_and_instances, ctx, raise_on_unsortable=False): - ''' + """ Sort instances topologically based on their dependency declarations. - ''' + """ sorted_list = [] variables = set(ctx.keys()) unsorted = list(source_and_instances) @@ -66,17 +67,21 @@ def topological_sort(source_and_instances, ctx, raise_on_unsortable=False): for source, instance in unsorted: dependencies = set(source.get_dependencies(instance, ctx)) sorted_list.append((source, instance)) - logger.debug('missing dependencies for instance %r of %r: %s', instance, source, - list(dependencies - variables)) + logger.debug( + 'missing dependencies for instance %r of %r: %s', + instance, + source, + list(dependencies - variables), + ) break return sorted_list @to_list def get_sources(): - ''' + """ List all known sources - ''' + """ for path in app_settings.ATTRIBUTE_BACKENDS: yield utils.import_module_or_class(path) for plugin in plugins.get_plugins(): @@ -87,9 +92,9 @@ def get_sources(): @to_list def get_attribute_names(ctx): - ''' + """ Return attribute names from all sources - ''' + """ for source in get_sources(): for instance in source.get_instances(ctx): for attribute_name, attribute_description in source.get_attribute_names(instance, ctx): @@ -97,12 +102,12 @@ def get_attribute_names(ctx): def get_attributes(ctx): - ''' + """ Traverse and sources instances and aggregate produced attributes. Traversal is done by respecting a topological sort of instances based on their declared dependencies - ''' + """ source_and_instances = [] for source in get_sources(): source_and_instances.extend(((source, instance) for instance in source.get_instances(ctx))) @@ -116,5 +121,8 @@ def get_attributes(ctx): @to_iter def get_service_attributes(service): ctx = {'request': None, 'user': None, 'service': service} - return ([('', _('None'))] + get_attribute_names(ctx) - + [('@verified_attributes@', _('List of verified attributes'))]) + return ( + [('', _('None'))] + + get_attribute_names(ctx) + + [('@verified_attributes@', _('List of verified attributes'))] + ) diff --git a/src/authentic2/attributes_ng/sources/__init__.py b/src/authentic2/attributes_ng/sources/__init__.py index a06b262d..1895f09e 100644 --- a/src/authentic2/attributes_ng/sources/__init__.py +++ b/src/authentic2/attributes_ng/sources/__init__.py @@ -21,9 +21,10 @@ from django.utils import six @six.add_metaclass(abc.ABCMeta) class BaseAttributeSource(object): - ''' + """ Base class for attribute sources - ''' + """ + @abc.abstractmethod def get_instances(self, ctx): pass diff --git a/src/authentic2/attributes_ng/sources/django_user.py b/src/authentic2/attributes_ng/sources/django_user.py index 07392d21..0537016c 100644 --- a/src/authentic2/attributes_ng/sources/django_user.py +++ b/src/authentic2/attributes_ng/sources/django_user.py @@ -27,9 +27,9 @@ from ...decorators import to_list @to_list def get_instances(ctx): - ''' + """ Retrieve instances from settings - ''' + """ return [None] diff --git a/src/authentic2/attributes_ng/sources/format.py b/src/authentic2/attributes_ng/sources/format.py index dbe36118..507075aa 100644 --- a/src/authentic2/attributes_ng/sources/format.py +++ b/src/authentic2/attributes_ng/sources/format.py @@ -25,16 +25,18 @@ AUTHORIZED_KEYS = set(('name', 'label', 'template')) @to_list def get_field_refs(format_string): - ''' + """ Extract the base references from format_string - ''' + """ from string import Formatter + l = Formatter().parse(format_string) # noqa: E741 for p in l: field_ref = p[1].split('[', 1)[0] field_ref = field_ref.split('.', 1)[0] yield field_ref + UNEXPECTED_KEYS_ERROR = '{0}: unexpected ' 'key(s) {1} in configuration' FORMAT_STRING_ERROR = '{0}: template string must contain only keyword references: {1}' BAD_CONFIG_ERROR = 'template attribute source must contain a name and at least a template' @@ -47,10 +49,11 @@ def config_error(fmt, *args): @to_list def get_instances(ctx): - ''' + """ Retrieve instances from settings - ''' + """ from django.conf import settings + for kind, d in getattr(settings, 'ATTRIBUTE_SOURCES', []): if kind != 'template': continue diff --git a/src/authentic2/attributes_ng/sources/function.py b/src/authentic2/attributes_ng/sources/function.py index 69c06201..743095b9 100644 --- a/src/authentic2/attributes_ng/sources/function.py +++ b/src/authentic2/attributes_ng/sources/function.py @@ -35,10 +35,11 @@ def config_error(fmt, *args): @to_list def get_instances(ctx): - ''' + """ Retrieve instances from settings - ''' + """ from django.conf import settings + for kind, d in getattr(settings, 'ATTRIBUTE_SOURCES', []): if kind != 'function': continue @@ -50,8 +51,9 @@ def get_instances(ctx): missing = REQUIRED_KEYS - keys config_error(MISSING_KEYS_ERROR, missing) dependencies = d['dependencies'] - if not isinstance(dependencies, (list, tuple)) or \ - not all(isinstance(dep, str) for dep in dependencies): + if not isinstance(dependencies, (list, tuple)) or not all( + isinstance(dep, str) for dep in dependencies + ): config_error(DEPENDENCY_TYPE_ERROR) if not callable(d['function']): diff --git a/src/authentic2/attributes_ng/sources/ldap.py b/src/authentic2/attributes_ng/sources/ldap.py index bc095b1d..ce0256c5 100644 --- a/src/authentic2/attributes_ng/sources/ldap.py +++ b/src/authentic2/attributes_ng/sources/ldap.py @@ -21,9 +21,9 @@ from authentic2.backends.ldap_backend import LDAPBackend, LDAPUser @to_list def get_instances(ctx): - ''' + """ Retrieve instances from settings - ''' + """ return [None] diff --git a/src/authentic2/attributes_ng/sources/service_roles.py b/src/authentic2/attributes_ng/sources/service_roles.py index cc5ef5d1..b001857d 100644 --- a/src/authentic2/attributes_ng/sources/service_roles.py +++ b/src/authentic2/attributes_ng/sources/service_roles.py @@ -33,8 +33,7 @@ def get_attribute_names(instance, ctx): if not isinstance(service, Service): return names = [] - for service_role in Role.objects.filter(service=service) \ - .prefetch_related('attributes'): + for service_role in Role.objects.filter(service=service).prefetch_related('attributes'): for service_role_attribute in service_role.attributes.all(): if service_role_attribute.name in names: continue @@ -45,7 +44,10 @@ def get_attribute_names(instance, ctx): def get_dependencies(instance, ctx): - return ('user', 'service',) + return ( + 'user', + 'service', + ) def get_attributes(instance, ctx): @@ -54,9 +56,7 @@ def get_attributes(instance, ctx): if not user or not service: return ctx ctx = ctx.copy() - roles = Role.objects.for_user(user) \ - .filter(service=service) \ - .prefetch_related('attributes') + roles = Role.objects.for_user(user).filter(service=service).prefetch_related('attributes') for service_role in roles: for service_role_attribute in service_role.attributes.all(): name = service_role_attribute.name diff --git a/src/authentic2/auth2_auth/auth2_ssl/migrations/0001_initial.py b/src/authentic2/auth2_auth/auth2_ssl/migrations/0001_initial.py index 34ba0765..2f330ca6 100644 --- a/src/authentic2/auth2_auth/auth2_ssl/migrations/0001_initial.py +++ b/src/authentic2/auth2_auth/auth2_ssl/migrations/0001_initial.py @@ -7,22 +7,24 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('auth', '0002_auto_20150323_1720'), + ('auth', '0002_auto_20150323_1720'), ] operations = [ migrations.CreateModel( name='ClientCertificate', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('serial', models.CharField(max_length=255, blank=True)), ('subject_dn', models.CharField(max_length=255)), ('issuer_dn', models.CharField(max_length=255)), ('cert', models.TextField()), ('user', models.ForeignKey(to='auth.User')), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/src/authentic2/auth_migrations_18/0001_initial.py b/src/authentic2/auth_migrations_18/0001_initial.py index 74a0fbfd..916f2741 100644 --- a/src/authentic2/auth_migrations_18/0001_initial.py +++ b/src/authentic2/auth_migrations_18/0001_initial.py @@ -16,9 +16,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Permission', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(max_length=50, verbose_name='name')), - ('content_type', models.ForeignKey(to='contenttypes.ContentType', to_field='id', on_delete=models.CASCADE)), + ( + 'content_type', + models.ForeignKey(to='contenttypes.ContentType', to_field='id', on_delete=models.CASCADE), + ), ('codename', models.CharField(max_length=100, verbose_name='codename')), ], options={ @@ -30,9 +36,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Group', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.CharField(unique=True, max_length=80, verbose_name='name')), - ('permissions', models.ManyToManyField(to='auth.Permission', verbose_name='permissions', blank=True)), + ( + 'permissions', + models.ManyToManyField(to='auth.Permission', verbose_name='permissions', blank=True), + ), ], options={ 'verbose_name': 'group', @@ -42,19 +54,74 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(default=timezone.now, 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(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])), + ( + '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( + help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', + unique=True, + max_length=30, + verbose_name='username', + validators=[ + validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid') + ], + ), + ), ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), ('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)), - ('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')), + ( + '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=timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', related_name='+', related_query_name='+')), - ('user_permissions', models.ManyToManyField(to='auth.Permission', verbose_name='user permissions', blank=True, help_text='Specific permissions for this user.', related_name='+', related_query_name='+')), + ( + 'groups', + models.ManyToManyField( + to='auth.Group', + verbose_name='groups', + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', + related_name='+', + related_query_name='+', + ), + ), + ( + 'user_permissions', + models.ManyToManyField( + to='auth.Permission', + verbose_name='user permissions', + blank=True, + help_text='Specific permissions for this user.', + related_name='+', + related_query_name='+', + ), + ), ], options={ 'verbose_name': 'user', diff --git a/src/authentic2/auth_migrations_18/0002_auto_20150323_1720.py b/src/authentic2/auth_migrations_18/0002_auto_20150323_1720.py index 9e5439bc..a226231f 100644 --- a/src/authentic2/auth_migrations_18/0002_auto_20150323_1720.py +++ b/src/authentic2/auth_migrations_18/0002_auto_20150323_1720.py @@ -15,7 +15,17 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='username', - field=models.CharField(help_text='Required, 255 characters or fewer. Only letters, numbers, and @, ., +, -, or _ characters.', unique=True, max_length=255, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')]), + field=models.CharField( + help_text='Required, 255 characters or fewer. Only letters, numbers, and @, ., +, -, or _ characters.', + unique=True, + max_length=255, + verbose_name='username', + validators=[ + django.core.validators.RegexValidator( + '^[\\w.@+-]+$', 'Enter a valid username.', 'invalid' + ) + ], + ), preserve_default=True, ), ] diff --git a/src/authentic2/auth_migrations_18/0003_auto_20150410_1657.py b/src/authentic2/auth_migrations_18/0003_auto_20150410_1657.py index 223af006..0e547a69 100644 --- a/src/authentic2/auth_migrations_18/0003_auto_20150410_1657.py +++ b/src/authentic2/auth_migrations_18/0003_auto_20150410_1657.py @@ -13,5 +13,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.DeleteModel('User'), + migrations.DeleteModel('User'), ] diff --git a/src/authentic2/auth_migrations_18/0004_user.py b/src/authentic2/auth_migrations_18/0004_user.py index b9de4ba6..2da7c25d 100644 --- a/src/authentic2/auth_migrations_18/0004_user.py +++ b/src/authentic2/auth_migrations_18/0004_user.py @@ -19,19 +19,82 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(default=django.utils.timezone.now, 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(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])), + ( + 'last_login', + models.DateTimeField(default=django.utils.timezone.now, 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( + help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', + unique=True, + max_length=30, + verbose_name='username', + validators=[ + django.core.validators.RegexValidator( + '^[\\w.@+-]+$', 'Enter a valid username.', 'invalid' + ) + ], + ), + ), ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), ('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)), - ('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')), - ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), + ( + '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'), + ), + ( + 'groups', + models.ManyToManyField( + related_query_name='user', + related_name='user_set', + to='auth.Group', + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', + verbose_name='groups', + ), + ), + ( + 'user_permissions', + models.ManyToManyField( + related_query_name='user', + related_name='user_set', + to='auth.Permission', + blank=True, + help_text='Specific permissions for this user.', + verbose_name='user permissions', + ), + ), ], options={ 'abstract': False, diff --git a/src/authentic2/auth_migrations_18/0005_auto_20150526_2303.py b/src/authentic2/auth_migrations_18/0005_auto_20150526_2303.py index c6b6f3fd..52c018d7 100644 --- a/src/authentic2/auth_migrations_18/0005_auto_20150526_2303.py +++ b/src/authentic2/auth_migrations_18/0005_auto_20150526_2303.py @@ -44,7 +44,14 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='groups', - field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), + field=models.ManyToManyField( + related_query_name='user', + related_name='user_set', + to='auth.Group', + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + verbose_name='groups', + ), ), migrations.AlterField( model_name='user', @@ -54,6 +61,19 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='username', - field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'), + field=models.CharField( + error_messages={'unique': 'A user with that username already exists.'}, + max_length=30, + validators=[ + django.core.validators.RegexValidator( + '^[\\w.@+-]+$', + 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', + 'invalid', + ) + ], + help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', + unique=True, + verbose_name='username', + ), ), ] diff --git a/src/authentic2/authentication.py b/src/authentic2/authentication.py index a0980ad5..6cfb3a3d 100644 --- a/src/authentic2/authentication.py +++ b/src/authentic2/authentication.py @@ -17,6 +17,7 @@ import inspect from django.utils import six + try: from django.utils.deprecation import CallableTrue except ImportError: @@ -29,8 +30,7 @@ from rest_framework.authentication import BasicAuthentication class OIDCUser(object): - """ Fake user class to return in case OIDC authentication - """ + """Fake user class to return in case OIDC authentication""" def __init__(self, oidc_client): self.oidc_client = oidc_client @@ -54,26 +54,29 @@ class OIDCUser(object): class Authentic2Authentication(BasicAuthentication): - def authenticate_credentials(self, userid, password, request=None): # try Simple OIDC Authentication try: client = OIDCClient.objects.get(client_id=userid, client_secret=password) if not client.has_api_access: raise AuthenticationFailed('OIDC client does not have access to the API') - if client.identifier_policy not in (client.POLICY_UUID, - client.POLICY_PAIRWISE_REVERSIBLE): - raise AuthenticationFailed('OIDC Client identifier policy does not allow access to ' - 'the API') + if client.identifier_policy not in (client.POLICY_UUID, client.POLICY_PAIRWISE_REVERSIBLE): + raise AuthenticationFailed( + 'OIDC Client identifier policy does not allow access to ' 'the API' + ) user = OIDCUser(client) user.authenticated = True return (user, True) except OIDCClient.DoesNotExist: pass # try BasicAuthentication - if (six.PY3 - and 'request' in inspect.signature( - super(Authentic2Authentication, self).authenticate_credentials).parameters): + if ( + six.PY3 + and 'request' + in inspect.signature(super(Authentic2Authentication, self).authenticate_credentials).parameters + ): # compatibility with DRF 3.4 - return super(Authentic2Authentication, self).authenticate_credentials(userid, password, request=request) + return super(Authentic2Authentication, self).authenticate_credentials( + userid, password, request=request + ) return super(Authentic2Authentication, self).authenticate_credentials(userid, password) diff --git a/src/authentic2/authenticators.py b/src/authentic2/authenticators.py index b92489ff..228fd3bf 100644 --- a/src/authentic2/authenticators.py +++ b/src/authentic2/authenticators.py @@ -32,7 +32,6 @@ logger = logging.getLogger(__name__) class BaseAuthenticator(object): - def __init__(self, show_condition=None, **kwargs): self.show_condition = show_condition @@ -71,7 +70,12 @@ class LoginPasswordAuthenticator(BaseAuthenticator): if not roles: return [] service_ou_ids = [] - qs = User.objects.filter(roles__in=roles).values_list('ou').annotate(count=Count('ou')).order_by('-count') + qs = ( + User.objects.filter(roles__in=roles) + .values_list('ou') + .annotate(count=Count('ou')) + .order_by('-count') + ) for ou_id, count in qs: if not ou_id: continue @@ -109,10 +113,8 @@ class LoginPasswordAuthenticator(BaseAuthenticator): initial['ou'] = preferred_ous[0] form = authentication_forms.AuthenticationForm( - request=request, - data=data, - initial=initial, - preferred_ous=preferred_ous) + request=request, data=data, initial=initial, preferred_ous=preferred_ous + ) if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION: form.fields['username'].label = _('Username or email') if app_settings.A2_USERNAME_LABEL: @@ -131,11 +133,15 @@ class LoginPasswordAuthenticator(BaseAuthenticator): request.session.set_expiry(app_settings.A2_USER_REMEMBER_ME) response = utils.login(request, form.get_user(), how, service=service) if 'ou' in form.fields: - utils.prepend_remember_cookie(request, response, 'preferred-ous', form.cleaned_data['ou'].pk) + utils.prepend_remember_cookie( + request, response, 'preferred-ous', form.cleaned_data['ou'].pk + ) if hasattr(request, 'needs_password_change'): del request.needs_password_change - return utils.redirect(request, 'password_change', params={'next': response.url}, resolve=True) + return utils.redirect( + request, 'password_change', params={'next': response.url}, resolve=True + ) return response else: diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index a967174e..797bddbc 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -26,6 +26,7 @@ try: from ldap.controls import SimplePagedResultsControl, DecodeControlTuples from ldap.controls import ppolicy from pyasn1.codec.der import decoder + PYTHON_LDAP3 = [int(x) for x in ldap.__version__.split('.')] >= [3] LDAPObject = NativeLDAPObject except ImportError: @@ -97,16 +98,30 @@ def filter_non_unicode_values(atvs): if PYTHON_LDAP3 is True: + class LDAPObject(NativeLDAPObject): - def __init__(self, uri, trace_level=0, trace_file=None, - trace_stack_limit=5, bytes_mode=False, - bytes_strictness=None, retry_max=1, retry_delay=60.0): - NativeLDAPObject.__init__(self, uri=uri, trace_level=trace_level, - trace_file=trace_file, - trace_stack_limit=trace_stack_limit, - bytes_mode=bytes_mode, bytes_strictness=bytes_strictness, - retry_max=retry_max, - retry_delay=retry_delay) + def __init__( + self, + uri, + trace_level=0, + trace_file=None, + trace_stack_limit=5, + bytes_mode=False, + bytes_strictness=None, + retry_max=1, + retry_delay=60.0, + ): + NativeLDAPObject.__init__( + self, + uri=uri, + trace_level=trace_level, + trace_file=trace_file, + trace_stack_limit=trace_stack_limit, + bytes_mode=bytes_mode, + bytes_strictness=bytes_strictness, + retry_max=retry_max, + retry_delay=retry_delay, + ) @to_list def _convert_results_to_unicode(self, result_list): @@ -119,11 +134,13 @@ if PYTHON_LDAP3 is True: def modify_s(self, dn, modlist): new_modlist = [] for mod_op, mod_typ, mod_vals in modlist: + def convert(v): if hasattr(v, 'isnumeric'): # unicode case v = v.encode('utf-8') return v + if mod_vals is None: pass elif isinstance(mod_vals, list): @@ -133,9 +150,24 @@ if PYTHON_LDAP3 is True: new_modlist.append((mod_op, mod_typ, mod_vals)) return NativeLDAPObject.modify_s(self, dn, new_modlist) - def result4(self, msgid=ldap.RES_ANY, all=1, timeout=None, add_ctrls=0, - add_intermediates=0, add_extop=0, resp_ctrl_classes=None): - resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value = NativeLDAPObject.result4( + def result4( + self, + msgid=ldap.RES_ANY, + all=1, + timeout=None, + add_ctrls=0, + add_intermediates=0, + add_extop=0, + resp_ctrl_classes=None, + ): + ( + resp_type, + resp_data, + resp_msgid, + decoded_resp_ctrls, + resp_name, + resp_value, + ) = NativeLDAPObject.result4( self, msgid=msgid, all=all, @@ -143,27 +175,30 @@ if PYTHON_LDAP3 is True: add_ctrls=add_ctrls, add_intermediates=add_intermediates, add_extop=add_extop, - resp_ctrl_classes=resp_ctrl_classes) + resp_ctrl_classes=resp_ctrl_classes, + ) if resp_data: resp_data = self._convert_results_to_unicode(resp_data) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value + elif PYTHON_LDAP3 is False: + class LDAPObject(NativeLDAPObject): def simple_bind_s(self, who='', cred='', serverctrls=None, clientctrls=None): who = force_bytes(who) cred = force_bytes(cred) - return NativeLDAPObject.simple_bind_s(self, who=who, cred=cred, - serverctrls=serverctrls, - clientctrls=clientctrls) + return NativeLDAPObject.simple_bind_s( + self, who=who, cred=cred, serverctrls=serverctrls, clientctrls=clientctrls + ) def passwd_s(self, dn, oldpw, newpw, serverctrls=None, clientctrls=None): dn = force_bytes(dn) oldpw = force_bytes(oldpw) newpw = force_bytes(newpw) - return NativeLDAPObject.passwd_s(self, dn, oldpw, newpw, - serverctrls=serverctrls, - clientctrls=clientctrls) + return NativeLDAPObject.passwd_s( + self, dn, oldpw, newpw, serverctrls=serverctrls, clientctrls=clientctrls + ) @to_list def _convert_results_to_unicode(self, result_list): @@ -173,21 +208,34 @@ elif PYTHON_LDAP3 is False: attrs = {attribute: filter_non_unicode_values(attrs[attribute]) for attribute in attrs} yield force_text(dn), attrs - def search_ext(self, base, scope, filterstr='(objectclass=*)', - attrlist=None, attrsonly=0, serverctrls=None, - clientctrls=None, timeout=-1, sizelimit=0): + def search_ext( + self, + base, + scope, + filterstr='(objectclass=*)', + attrlist=None, + attrsonly=0, + serverctrls=None, + clientctrls=None, + timeout=-1, + sizelimit=0, + ): base = force_bytes(base) filterstr = force_bytes(filterstr) if attrlist: attrlist = [force_bytes(attr) for attr in attrlist] - return NativeLDAPObject.search_ext(self, base, scope, - filterstr=filterstr, - attrlist=attrlist, - attrsonly=attrsonly, - serverctrls=serverctrls, - clientctrls=clientctrls, - timeout=timeout, - sizelimit=sizelimit) + return NativeLDAPObject.search_ext( + self, + base, + scope, + filterstr=filterstr, + attrlist=attrlist, + attrsonly=attrsonly, + serverctrls=serverctrls, + clientctrls=clientctrls, + timeout=timeout, + sizelimit=sizelimit, + ) def modify_s(self, dn, modlist): dn = force_bytes(dn) @@ -200,6 +248,7 @@ elif PYTHON_LDAP3 is False: # unicode case v = force_bytes(v) return v + if mod_vals is None: pass elif isinstance(mod_vals, list): @@ -209,9 +258,24 @@ elif PYTHON_LDAP3 is False: new_modlist.append((mod_op, mod_typ, mod_vals)) return NativeLDAPObject.modify_s(self, dn, new_modlist) - def result4(self, msgid=ldap.RES_ANY, all=1, timeout=None, add_ctrls=0, - add_intermediates=0, add_extop=0, resp_ctrl_classes=None): - resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value = NativeLDAPObject.result4( + def result4( + self, + msgid=ldap.RES_ANY, + all=1, + timeout=None, + add_ctrls=0, + add_intermediates=0, + add_extop=0, + resp_ctrl_classes=None, + ): + ( + resp_type, + resp_data, + resp_msgid, + decoded_resp_ctrls, + resp_name, + resp_value, + ) = NativeLDAPObject.result4( self, msgid=msgid, all=all, @@ -219,7 +283,8 @@ elif PYTHON_LDAP3 is False: add_ctrls=add_ctrls, add_intermediates=add_intermediates, add_extop=add_extop, - resp_ctrl_classes=resp_ctrl_classes) + resp_ctrl_classes=resp_ctrl_classes, + ) if resp_data: resp_data = self._convert_results_to_unicode(resp_data) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value @@ -258,15 +323,20 @@ def password_policy_control_messages(ctrl): if ctrl.timeBeforeExpiration: expiration_date = time.asctime(time.localtime(time.time() + ctrl.timeBeforeExpiration)) - messages.append(_('The password will expire at {expiration_date}.').format( - expiration_date=expiration_date)) + messages.append( + _('The password will expire at {expiration_date}.').format(expiration_date=expiration_date) + ) if ctrl.graceAuthNsRemaining: - messages.append(ngettext( - 'This password expired: this is the last time it can be used.', - 'This password expired and can only be used {graceAuthNsRemaining} times, including this one.', - ctrl.graceAuthNsRemaining).format(graceAuthNsRemaining=ctrl.graceAuthNsRemaining)) + messages.append( + ngettext( + 'This password expired: this is the last time it can be used.', + 'This password expired and can only be used {graceAuthNsRemaining} times, including this one.', + ctrl.graceAuthNsRemaining, + ).format(graceAuthNsRemaining=ctrl.graceAuthNsRemaining) + ) return messages + class LDAPUser(User): SESSION_LDAP_DATA_KEY = 'ldap-data' _changed = False @@ -295,13 +365,16 @@ class LDAPUser(User): # update dn case, can be removed in the future self.ldap_data['dn'] = self.ldap_data['dn'].lower() if self.ldap_data.get('password'): - self.ldap_data['password'] = {key.lower(): value for key, value in self.ldap_data['password'].items()} + self.ldap_data['password'] = { + key.lower(): value for key, value in self.ldap_data['password'].items() + } # retrieve encrypted bind pw if necessary encrypted_bindpw = self.ldap_data.get('block', {}).get('encrypted_bindpw') if encrypted_bindpw: - decrypted = crypto.aes_base64_decrypt(settings.SECRET_KEY, encrypted_bindpw, - raise_on_error=False) + decrypted = crypto.aes_base64_decrypt( + settings.SECRET_KEY, encrypted_bindpw, raise_on_error=False + ) if decrypted: decrypted = force_text(decrypted) self.ldap_data['block']['bindpw'] = decrypted @@ -312,8 +385,9 @@ class LDAPUser(User): data = dict(self.ldap_data) data['block'] = dict(data['block']) if data['block'].get('bindpw'): - data['block']['encrypted_bindpw'] = force_text(crypto.aes_base64_encrypt( - settings.SECRET_KEY, force_bytes(data['block']['bindpw']))) + data['block']['encrypted_bindpw'] = force_text( + crypto.aes_base64_encrypt(settings.SECRET_KEY, force_bytes(data['block']['bindpw'])) + ) del data['block']['bindpw'] session[self.SESSION_LDAP_DATA_KEY] = data @@ -346,8 +420,7 @@ class LDAPUser(User): cache = self.ldap_data.setdefault('password', {}) if password is not None: # Prevent eavesdropping of the password through the session storage - password = force_text(crypto.aes_base64_encrypt( - settings.SECRET_KEY, force_bytes(password))) + password = force_text(crypto.aes_base64_encrypt(settings.SECRET_KEY, force_bytes(password))) cache[self.dn] = password # ensure session is marked dirty self.update_request() @@ -420,7 +493,9 @@ class LDAPUser(User): return self.ldap_backend.get_connection(self.block, credentials=credentials) def get_attributes(self, attribute_source, ctx): - cache_key = hashlib.md5((force_text(str(self.pk)) + ';' + force_text(self.dn)).encode('utf-8')).hexdigest() + cache_key = hashlib.md5( + (force_text(str(self.pk)) + ';' + force_text(self.dn)).encode('utf-8') + ).hexdigest() conn = self.get_connection() # prevents blocking on temporary LDAP failures if conn is not None: @@ -506,7 +581,10 @@ class LDAPBackend(object): 'update_username': False, # lookup existing user with an external id build with attributes 'lookups': ('external_id', 'username'), - 'external_id_tuples': (('uid',), ('dn:noquote',),), + 'external_id_tuples': ( + ('uid',), + ('dn:noquote',), + ), # clean all other existing external id for an user after linking the user # to an external id. 'clean_external_id_on_update': True, @@ -532,10 +610,8 @@ class LDAPBackend(object): 'certfile': '', 'keyfile': '', # LDAP library options - 'ldap_options': { - }, - 'global_ldap_options': { - }, + 'ldap_options': {}, + 'global_ldap_options': {}, # Use Password Modify extended operation 'use_password_modify': True, # Target OU @@ -551,10 +627,18 @@ class LDAPBackend(object): } _REQUIRED = ('url', 'basedn') _TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive') - _TO_LOWERCASE = ('fname_field', 'lname_field', 'email_field', 'attributes', - 'mandatory_attributes_values', 'member_of_attribute', - 'group_to_role_mapping', 'group_mapping', - 'attribute_mappings', 'external_id_tuples') + _TO_LOWERCASE = ( + 'fname_field', + 'lname_field', + 'email_field', + 'attributes', + 'mandatory_attributes_values', + 'member_of_attribute', + 'group_to_role_mapping', + 'group_mapping', + 'attribute_mappings', + 'external_id_tuples', + ) _VALID_CONFIG_KEYS = list(set(_REQUIRED).union(set(_DEFAULTS))) @classmethod @@ -607,8 +691,7 @@ class LDAPBackend(object): def get_groups_dns(cls, conn, block): group_base_dn = block['group_basedn'] or block['basedn'] # 1.1 is special attribute meaning, "no attribute requested" - results = conn.search_s(group_base_dn, ldap.SCOPE_SUBTREE, - block['group_filter'], ['1.1']) + results = conn.search_s(group_base_dn, ldap.SCOPE_SUBTREE, block['group_filter'], ['1.1']) results = cls.normalize_ldap_results(results) return set([group_dn for group_dn, attrs in results]) @@ -638,8 +721,7 @@ class LDAPBackend(object): if realm and block.get('realm') != realm: continue if '%s' not in block['user_filter']: - log.error( - "account name authentication filter doesn't contain '%s'") + log.error("account name authentication filter doesn't contain '%s'") continue user = self.authenticate_block(request, block, uid, password) if user is not None: @@ -667,11 +749,11 @@ class LDAPBackend(object): try: query = filter_format(user_filter, (username,) * n) except TypeError as e: - log.error('user_filter syntax error %r: %s', block['user_filter'], - e) + log.error('user_filter syntax error %r: %s', block['user_filter'], e) return - log.debug('[%s] looking up dn for username %r using query %r', ldap_uri, - username, query) + log.debug( + '[%s] looking up dn for username %r using query %r', ldap_uri, username, query + ) results = conn.search_s(user_basedn, ldap.SCOPE_SUBTREE, query, [u'1.1']) results = self.normalize_ldap_results(results) # remove search references @@ -680,8 +762,12 @@ class LDAPBackend(object): if len(results) == 0: log.debug('[%s] user lookup failed: no entry found, %s', ldap_uri, query) elif not block['multimatch'] and len(results) > 1: - log.error('[%s] user lookup failed: too many (%d) entries found: %s', - ldap_uri, len(results), query) + log.error( + '[%s] user lookup failed: too many (%d) entries found: %s', + ldap_uri, + len(results), + query, + ) else: authz_ids.extend(result[0] for result in results) else: @@ -719,7 +805,9 @@ class LDAPBackend(object): break except ldap.INVALID_CREDENTIALS as e: if block.get('use_controls') and len(e.args) > 0 and 'ctrls' in e.args[0]: - self.process_controls(request, authz_id, DecodeControlTuples(e.args[0]['ctrls'])) + self.process_controls( + request, authz_id, DecodeControlTuples(e.args[0]['ctrls']) + ) attributes = self.get_ldap_attributes(block, conn, authz_id) user = self.lookup_existing_user(authz_id, block, attributes) if user and hasattr(request, 'failed_logins'): @@ -738,8 +826,11 @@ class LDAPBackend(object): break return self._return_user(authz_id, password, conn, block) except ldap.CONNECT_ERROR: - log.error('connection to %r failed, did you forget to declare the TLS certificate ' - 'in /etc/ldap/ldap.conf ?', block['url']) + log.error( + 'connection to %r failed, did you forget to declare the TLS certificate ' + 'in /etc/ldap/ldap.conf ?', + block['url'], + ) except ldap.TIMEOUT: log.error('connection to %r timed out', block['url']) except ldap.SERVER_DOWN: @@ -767,8 +858,9 @@ class LDAPBackend(object): @classmethod def _parse_simple_config(self): if len(settings.LDAP_AUTH_SETTINGS) < 2: - raise ImproperlyConfigured('In a minimal configuration, you must at least specify ' - 'url and user DN') + raise ImproperlyConfigured( + 'In a minimal configuration, you must at least specify ' 'url and user DN' + ) return {'url': settings.LDAP_AUTH_SETTINGS[0], 'basedn': settings.LDAP_AUTH_SETTINGS[1]} def backend_name(self): @@ -786,9 +878,11 @@ class LDAPBackend(object): def populate_user_attributes(self, user, block, attributes): # map legacy attributes (columns from Django user model) - for legacy_attribute, legacy_field in (('email', 'email_field'), - ('first_name', 'fname_field'), - ('last_name', 'lname_field')): + for legacy_attribute, legacy_field in ( + ('email', 'email_field'), + ('first_name', 'fname_field'), + ('last_name', 'lname_field'), + ): ldap_attribute = block[legacy_field] if not ldap_attribute: break @@ -820,12 +914,14 @@ class LDAPBackend(object): user._changed = True def populate_admin_flags_by_group(self, user, block, group_dns): - '''Attribute admin flags based on groups. - - It supersedes is_staff, is_superuser and is_active.''' - for g, attr in (('groupsu', 'is_superuser'), - ('groupstaff', 'is_staff'), - ('groupactive', 'is_active')): + """Attribute admin flags based on groups. + + It supersedes is_staff, is_superuser and is_active.""" + for g, attr in ( + ('groupsu', 'is_superuser'), + ('groupstaff', 'is_staff'), + ('groupactive', 'is_active'), + ): group_dns_to_match = block[g] if not group_dns_to_match: continue @@ -887,9 +983,9 @@ class LDAPBackend(object): role.save() def get_ldap_group_dns(self, user, dn, conn, block, attributes): - '''Retrieve group DNs from the LDAP by attributes (memberOf) or by - filter. - ''' + """Retrieve group DNs from the LDAP by attributes (memberOf) or by + filter. + """ group_base_dn = block['group_basedn'] or block['basedn'] member_of_attribute = block['member_of_attribute'] group_filter = block['group_filter'] @@ -957,13 +1053,15 @@ class LDAPBackend(object): try: return Role.objects.get(name=slug, **kwargs), None except Role.DoesNotExist: - error = ('role %r does not exist' % role_id) + error = 'role %r does not exist' % role_id except Role.MultipleObjectsReturned: error = 'multiple objects returned, identifier is imprecise' except Role.MultipleObjectsReturned: error = 'multiple objects returned, identifier is imprecise' else: - error = 'invalid role identifier must be slug, (slug, ou__slug) or (slug, ou__slug, service__slug)' + error = ( + 'invalid role identifier must be slug, (slug, ou__slug) or (slug, ou__slug, service__slug)' + ) return None, error def populate_mandatory_groups(self, user, block): @@ -1018,8 +1116,8 @@ class LDAPBackend(object): self.populate_user_roles(user, dn, conn, block, attributes) def populate_user_ou(self, user, dn, conn, block, attributes): - '''Assign LDAP user to an ou, the default one if ou_slug setting is - None''' + """Assign LDAP user to an ou, the default one if ou_slug setting is + None""" ou_slug = block['ou_slug'] OU = get_ou_model() @@ -1052,13 +1150,11 @@ class LDAPBackend(object): def get_ldap_attributes_names(cls, block): attributes = set() attributes.update(map_text(block['attributes'])) - for field in ('email_field', 'fname_field', 'lname_field', - 'member_of_attribute'): + for field in ('email_field', 'fname_field', 'lname_field', 'member_of_attribute'): if block[field]: attributes.add(block[field]) for external_id_tuple in map_text(block['external_id_tuples']): - attributes.update(cls.attribute_name_from_external_id_tuple( - external_id_tuple)) + attributes.update(cls.attribute_name_from_external_id_tuple(external_id_tuple)) for from_at, to_at in map_text(block['attribute_mappings']): attributes.add(to_at) for mapping in block['user_attributes']: @@ -1076,8 +1172,8 @@ class LDAPBackend(object): @classmethod def get_ldap_attributes(cls, block, conn, dn): - '''Retrieve some attributes from LDAP, add mandatory values then apply - defined mappings between atrribute names''' + """Retrieve some attributes from LDAP, add mandatory values then apply + defined mappings between atrribute names""" attributes = cls.get_ldap_attributes_names(block) attribute_mappings = map_text(block['attribute_mappings']) mandatory_attributes_values = map_text(block['mandatory_attributes_values']) @@ -1125,33 +1221,55 @@ class LDAPBackend(object): extra_attribute_config = block['extra_attributes'][extra_attribute_name] extra_attribute_values = [] if 'loop_over_attribute' in extra_attribute_config: - extra_attribute_config['loop_over_attribute'] = extra_attribute_config['loop_over_attribute'].lower() + extra_attribute_config['loop_over_attribute'] = extra_attribute_config[ + 'loop_over_attribute' + ].lower() if extra_attribute_config['loop_over_attribute'] not in attribute_map: - log.debug('loop_over_attribute %s not present (or empty) in user object attributes retreived. Pass.' % extra_attribute_config['loop_over_attribute']) + log.debug( + 'loop_over_attribute %s not present (or empty) in user object attributes retreived. Pass.' + % extra_attribute_config['loop_over_attribute'] + ) continue if 'filter' not in extra_attribute_config and 'basedn' not in extra_attribute_config: - log.warning('Extra attribute %s not correctly configured : you need to defined at least one of filter or basedn parameters' % extra_attribute_name) + log.warning( + 'Extra attribute %s not correctly configured : you need to defined at least one of filter or basedn parameters' + % extra_attribute_name + ) for item in attribute_map[extra_attribute_config['loop_over_attribute']]: - ldap_filter = extra_attribute_config.get('filter', 'objectClass=*').format(item=item, **attribute_map) - ldap_basedn = extra_attribute_config.get('basedn', block.get('basedn')).format(item=item, **attribute_map) - ldap_scope = ldap_scopes.get(extra_attribute_config.get('scope', 'sub'), ldap.SCOPE_SUBTREE) + ldap_filter = extra_attribute_config.get('filter', 'objectClass=*').format( + item=item, **attribute_map + ) + ldap_basedn = extra_attribute_config.get('basedn', block.get('basedn')).format( + item=item, **attribute_map + ) + ldap_scope = ldap_scopes.get( + extra_attribute_config.get('scope', 'sub'), ldap.SCOPE_SUBTREE + ) ldap_attributes_mapping = extra_attribute_config.get('mapping', {}) - ldap_attributes_names = list(filter(lambda a: a != 'dn', ldap_attributes_mapping.values())) + ldap_attributes_names = list( + filter(lambda a: a != 'dn', ldap_attributes_mapping.values()) + ) try: results = conn.search_s(ldap_basedn, ldap_scope, ldap_filter, ldap_attributes_names) except ldap.LDAPError: - log.exception('unable to retrieve extra attribute %s for item %s' % (extra_attribute_name, item)) + log.exception( + 'unable to retrieve extra attribute %s for item %s' % (extra_attribute_name, item) + ) continue else: results = cls.normalize_ldap_results(results) item_value = {} for dn, attrs in results: - log.debug(u'Object retrieved for extra attr %s with item %s : %s %s' % ( - extra_attribute_name, item, dn, attrs)) + log.debug( + u'Object retrieved for extra attr %s with item %s : %s %s' + % (extra_attribute_name, item, dn, attrs) + ) for key in ldap_attributes_mapping: item_value[key] = attrs.get(ldap_attributes_mapping[key].lower()) - log.debug('Object attribute %s value retrieved for extra attr %s with item %s : %s' % ( - ldap_attributes_mapping[key], extra_attribute_name, item, item_value[key])) + log.debug( + 'Object attribute %s value retrieved for extra attr %s with item %s : %s' + % (ldap_attributes_mapping[key], extra_attribute_name, item, item_value[key]) + ) if not item_value[key]: del item_value[key] elif len(item_value[key]) == 1: @@ -1165,14 +1283,17 @@ class LDAPBackend(object): elif extra_attribute_serialization == 'json': attribute_map[extra_attribute_name] = json.dumps(extra_attribute_values) else: - log.warning('Invalid serialization type "%s" for extra attribute %s' % (extra_attribute_serialization, extra_attribute_name)) + log.warning( + 'Invalid serialization type "%s" for extra attribute %s' + % (extra_attribute_serialization, extra_attribute_name) + ) return attribute_map @classmethod def external_id_to_filter(cls, external_id, external_id_tuple): - '''Split the external id, decode it and build an LDAP filter from it - and the external_id_tuple. - ''' + """Split the external id, decode it and build an LDAP filter from it + and the external_id_tuple. + """ splitted = external_id.split() if len(splitted) != len(external_id_tuple): return @@ -1191,9 +1312,9 @@ class LDAPBackend(object): return u'(&{0})'.format(''.join(filters)) def build_external_id(self, external_id_tuple, attributes): - '''Build the exernal id for the user, use attribute that eventually - never change like GUID or UUID. - ''' + """Build the exernal id for the user, use attribute that eventually + never change like GUID or UUID. + """ parts = [] for attribute in external_id_tuple: quote = True @@ -1221,16 +1342,23 @@ class LDAPBackend(object): if not external_id: continue log.debug('lookup using external_id %r: %r', eid_tuple, external_id) - users = LDAPUser.objects.prefetch_related('groups').filter( - userexternalid__external_id__iexact=external_id, - userexternalid__source=force_text(block['realm'])).order_by('-last_login') + users = ( + LDAPUser.objects.prefetch_related('groups') + .filter( + userexternalid__external_id__iexact=external_id, + userexternalid__source=force_text(block['realm']), + ) + .order_by('-last_login') + ) # ordering of NULLs cannot be done through the ORM users = sorted(users, reverse=True, key=lambda u: (u.last_login is not None, u.last_login)) if users: user = users[0] if len(users) > 1: - log.info('found %d users, collectings roles into the first one and deleting the other ones.', - len(users)) + log.info( + 'found %d users, collectings roles into the first one and deleting the other ones.', + len(users), + ) for other in users[1:]: for r in other.roles.all(): user.roles.add(r) @@ -1255,23 +1383,23 @@ class LDAPBackend(object): log_msg = 'updating username from %r to %r' log.debug(log_msg, old_username, user.username) # if external_id lookup is used, update it - if 'external_id' in block['lookups'] \ - and block.get('external_id_tuples') \ - and block['external_id_tuples'][0]: + if ( + 'external_id' in block['lookups'] + and block.get('external_id_tuples') + and block['external_id_tuples'][0] + ): if not user.pk: user.save() user._changed = False - external_id = self.build_external_id( - map_text(block['external_id_tuples'][0]), - attributes) + external_id = self.build_external_id(map_text(block['external_id_tuples'][0]), attributes) if external_id: new, created = UserExternalId.objects.get_or_create( - user=user, external_id=external_id, source=force_text(block['realm'])) + user=user, external_id=external_id, source=force_text(block['realm']) + ) if block['clean_external_id_on_update']: - UserExternalId.objects \ - .exclude(id=new.id) \ - .filter(user=user, source=force_text(block['realm'])) \ - .delete() + UserExternalId.objects.exclude(id=new.id).filter( + user=user, source=force_text(block['realm']) + ).delete() def _return_user(self, dn, password, conn, block, attributes=None): attributes = attributes or self.get_ldap_attributes(block, conn, dn) @@ -1345,26 +1473,32 @@ class LDAPBackend(object): user_basedn = force_text(block.get('user_basedn') or block['basedn']) user_filter = cls.get_sync_ldap_user_filter(block) attribute_names = cls.get_ldap_attributes_names(block) - results = cls.paged_search(conn, user_basedn, ldap.SCOPE_SUBTREE, user_filter, attrlist=attribute_names) + results = cls.paged_search( + conn, user_basedn, ldap.SCOPE_SUBTREE, user_filter, attrlist=attribute_names + ) backend = cls() for dn, attrs in results: yield backend._return_user(dn, None, conn, block, attrs) - @classmethod def deactivate_orphaned_users(cls): for block in cls.get_config(): conn = cls.get_connection(block) if conn is None: continue - eids = list(UserExternalId.objects.filter(user__is_active=True, - source=block['realm']).values_list('external_id', flat=True)) + eids = list( + UserExternalId.objects.filter(user__is_active=True, source=block['realm']).values_list( + 'external_id', flat=True + ) + ) basedn = force_text(block.get('user_basedn') or block['basedn']) - attribute_names = [a[0] for a in cls.attribute_name_from_external_id_tuple(block['external_id_tuples'])] + attribute_names = [ + a[0] for a in cls.attribute_name_from_external_id_tuple(block['external_id_tuples']) + ] user_filter = cls.get_sync_ldap_user_filter(block) - results = cls.paged_search(conn, basedn, ldap.SCOPE_SUBTREE, - user_filter, - attrlist=attribute_names) + results = cls.paged_search( + conn, basedn, ldap.SCOPE_SUBTREE, user_filter, attrlist=attribute_names + ) for dn, attrs in results: data = attrs.copy() data['dn'] = dn @@ -1379,7 +1513,6 @@ class LDAPBackend(object): for eid in UserExternalId.objects.filter(external_id__in=eids): eid.user.mark_as_inactive() - @classmethod def ad_encoding(cls, s): '''Encode a string for AD consumption as a password''' @@ -1398,7 +1531,7 @@ class LDAPBackend(object): if old_password: modlist = [ (ldap.MOD_DELETE, key, [cls.ad_encoding(old_password)]), - (ldap.MOD_ADD, key, [value]) + (ldap.MOD_ADD, key, [value]), ] else: modlist = [(ldap.MOD_REPLACE, key, [value])] @@ -1438,8 +1571,9 @@ class LDAPBackend(object): if block['timeout'] > 0: conn.set_option(ldap.OPT_NETWORK_TIMEOUT, block['timeout']) conn.set_option(ldap.OPT_TIMEOUT, block['timeout']) - conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, - getattr(ldap, 'OPT_X_TLS_' + block['require_cert'].upper())) + conn.set_option( + ldap.OPT_X_TLS_REQUIRE_CERT, getattr(ldap, 'OPT_X_TLS_' + block['require_cert'].upper()) + ) if block['cacertfile']: conn.set_option(ldap.OPT_X_TLS_CACERTFILE, block['cacertfile']) if block['cacertdir']: @@ -1458,15 +1592,21 @@ class LDAPBackend(object): try: conn.start_tls_s() except ldap.CONNECT_ERROR: - log.error('connection to %r failed when activating TLS, did you forget ' - 'to declare the TLS certificate in /etc/ldap/ldap.conf ?', url) + log.error( + 'connection to %r failed when activating TLS, did you forget ' + 'to declare the TLS certificate in /etc/ldap/ldap.conf ?', + url, + ) continue except ldap.TIMEOUT: log.error('connection to %r timed out', url) continue except ldap.CONNECT_ERROR: - log.error('connection to %r failed when activating TLS, did you forget to ' - 'declare the TLS certificate in /etc/ldap/ldap.conf ?', url) + log.error( + 'connection to %r failed when activating TLS, did you forget to ' + 'declare the TLS certificate in /etc/ldap/ldap.conf ?', + url, + ) continue except ldap.SERVER_DOWN: if block['replicas']: @@ -1529,12 +1669,13 @@ class LDAPBackend(object): if key not in cls._VALID_CONFIG_KEYS and validate: raise ImproperlyConfigured( '"{}" : invalid LDAP_AUTH_SETTINGS key, available are {}'.format( - key, cls._VALID_CONFIG_KEYS)) + key, cls._VALID_CONFIG_KEYS + ) + ) for r in cls._REQUIRED: if r not in block: - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: missing required configuration option %r' % r) + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: missing required configuration option %r' % r) # convert string to list of strings for settings accepting it for i in cls._TO_ITERABLE: @@ -1549,26 +1690,21 @@ class LDAPBackend(object): else: if isinstance(cls._DEFAULTS[d], six.string_types): if not isinstance(block[d], six.string_types): - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r must be a string' % d) + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: attribute %r must be a string' % d) try: block[d] = force_text(block[d]) except UnicodeEncodeError: - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r must be a string' % d) + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: attribute %r must be a string' % d) if isinstance(cls._DEFAULTS[d], bool) and not isinstance(block[d], bool): + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: attribute %r must be a boolean' % d) + if isinstance(cls._DEFAULTS[d], (list, tuple)) and not isinstance(block[d], (list, tuple)): raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r must be a boolean' % d) - if (isinstance(cls._DEFAULTS[d], (list, tuple)) - and not isinstance(block[d], (list, tuple))): - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r must be a list or a tuple' % d) + 'LDAP_AUTH_SETTINGS: attribute %r must be a list or a tuple' % d + ) if isinstance(cls._DEFAULTS[d], dict) and not isinstance(block[d], dict): - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r must be a dictionary' % d) + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: attribute %r must be a dictionary' % d) if not isinstance(cls._DEFAULTS[d], bool) and d in cls._REQUIRED and not block[d]: - raise ImproperlyConfigured( - 'LDAP_AUTH_SETTINGS: attribute %r is required but is empty') + raise ImproperlyConfigured('LDAP_AUTH_SETTINGS: attribute %r is required but is empty') # force_bytes all strings in iterable or dict if isinstance(block[d], (list, tuple, dict)): block[d] = map_text(block[d]) @@ -1600,7 +1736,8 @@ class LDAPBackend(object): else: raise NotImplementedError( 'LDAP setting %r cannot be converted to lowercase setting, its type is %r' - % (key, type(block[key]))) + % (key, type(block[key])) + ) # special case user_attributes user_attributes = [] for mapping in block['user_attributes']: @@ -1641,23 +1778,21 @@ class LDAPBackendPasswordLost(LDAPBackend): results = conn.search_s(dn, ldap.SCOPE_BASE) else: ldap_filter = self.external_id_to_filter(external_id, external_id_tuple) - results = conn.search_s(block['basedn'], - ldap.SCOPE_SUBTREE, ldap_filter) + results = conn.search_s(block['basedn'], ldap.SCOPE_SUBTREE, ldap_filter) results = self.normalize_ldap_results(results) if not results: log.warning( - u'unable to find user %r based on external id %s', - user, external_id) + u'unable to find user %r based on external id %s', user, external_id + ) continue dn = results[0][0] except ldap.LDAPError as e: log.warning( - u'unable to find user %r based on external id %s: %r', - user, - external_id, - e) + u'unable to find user %r based on external id %s: %r', user, external_id, e + ) continue return self._return_user(dn, None, conn, block) + LDAPUser.ldap_backend = LDAPBackend LDAPBackendPasswordLost.ldap_backend = LDAPBackend diff --git a/src/authentic2/backends/models_backend.py b/src/authentic2/backends/models_backend.py index 774bf44d..04ba1332 100644 --- a/src/authentic2/backends/models_backend.py +++ b/src/authentic2/backends/models_backend.py @@ -31,6 +31,7 @@ def upn(username, realm): '''Build an UPN from a username and a realm''' return u'{0}@{1}'.format(username, realm) + PROXY_USER_MODEL = None @@ -44,8 +45,7 @@ class ModelBackend(ModelBackend): username_field = 'username' queries = [] try: - if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION \ - and UserModel._meta.get_field('email'): + if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION and UserModel._meta.get_field('email'): queries.append(models.Q(**{'email__iexact': username})) except models.FieldDoesNotExist: pass @@ -55,8 +55,7 @@ class ModelBackend(ModelBackend): if '@' not in username: if app_settings.REALMS: for realm, desc in app_settings.REALMS: - queries.append(models.Q( - **{username_field: upn(username, realm)})) + queries.append(models.Q(**{username_field: upn(username, realm)})) else: queries.append(models.Q(**{username_field: upn(username, realm)})) queries = six.moves.reduce(models.Q.__or__, queries) @@ -66,6 +65,7 @@ class ModelBackend(ModelBackend): def must_reset_password(self, user): from .. import models + return bool(models.PasswordReset.filter(user=user).count()) def authenticate(self, request, username=None, password=None, realm=None, ou=None): @@ -96,6 +96,7 @@ class ModelBackend(ModelBackend): def get_saml2_authn_context(self): import lasso + return lasso.SAML2_AUTHN_CONTEXT_PASSWORD diff --git a/src/authentic2/cbv.py b/src/authentic2/cbv.py index b16d2ae8..b1edbbfa 100644 --- a/src/authentic2/cbv.py +++ b/src/authentic2/cbv.py @@ -26,12 +26,13 @@ from .utils.views import csrf_token_check class ValidateCSRFMixin(object): - '''Move CSRF token validation inside the form validation. + """Move CSRF token validation inside the form validation. + + This mixin must always be the leftest one and if your class override + form_valid() dispatch() you should move those overrides in a base + class. + """ - This mixin must always be the leftest one and if your class override - form_valid() dispatch() you should move those overrides in a base - class. - ''' @method_decorator(csrf_exempt) @method_decorator(ensure_csrf_cookie) def dispatch(self, *args, **kwargs): @@ -54,10 +55,11 @@ class RedirectToNextURLViewMixin(object): class NextURLViewMixin(RedirectToNextURLViewMixin): - '''Make a view handle a next parameter, if it's not present it is - automatically generated from the Referrer or from the value - returned by the method get_next_url_default(). - ''' + """Make a view handle a next parameter, if it's not present it is + automatically generated from the Referrer or from the value + returned by the method get_next_url_default(). + """ + next_url_default = '..' def get_next_url_default(self): @@ -67,15 +69,17 @@ class NextURLViewMixin(RedirectToNextURLViewMixin): if REDIRECT_FIELD_NAME in request.GET: pass else: - next_url = request.META.get('HTTP_REFERER') or \ - self.next_url_default - return utils.redirect(request, request.path, keep_params=True, - params={ - REDIRECT_FIELD_NAME: next_url, - }, - status=303) - return super(NextURLViewMixin, self).dispatch(request, *args, - **kwargs) + next_url = request.META.get('HTTP_REFERER') or self.next_url_default + return utils.redirect( + request, + request.path, + keep_params=True, + params={ + REDIRECT_FIELD_NAME: next_url, + }, + status=303, + ) + return super(NextURLViewMixin, self).dispatch(request, *args, **kwargs) class TemplateNamesMixin(object): diff --git a/src/authentic2/compat/cookies.py b/src/authentic2/compat/cookies.py index f426e928..ddb2e922 100644 --- a/src/authentic2/compat/cookies.py +++ b/src/authentic2/compat/cookies.py @@ -19,4 +19,5 @@ import django if django.VERSION < (2, 1): # Copied from Django >=2.1 / django.http.cookies from http import cookies + cookies.Morsel._reserved.setdefault('samesite', 'SameSite') diff --git a/src/authentic2/compat/misc.py b/src/authentic2/compat/misc.py index c0e90aa9..bbb44dc8 100644 --- a/src/authentic2/compat/misc.py +++ b/src/authentic2/compat/misc.py @@ -24,10 +24,12 @@ try: from django.contrib.auth import get_user_model except ImportError: from django.contrib.auth.models import User + get_user_model = lambda: User try: from django.db.transaction import atomic + commit_on_success = atomic except ImportError: from django.db.transaction import commit_on_success @@ -40,8 +42,12 @@ else: from binascii import Error as Base64Error if hasattr(inspect, 'signature'): + def signature_parameters(func): return inspect.signature(func).parameters.keys() + + else: + def signature_parameters(func): return inspect.getargspec(func)[0] diff --git a/src/authentic2/compat_lasso.py b/src/authentic2/compat_lasso.py index 3fb19764..2491f064 100644 --- a/src/authentic2/compat_lasso.py +++ b/src/authentic2/compat_lasso.py @@ -17,9 +17,11 @@ try: import lasso except ImportError: + class MockLasso(object): def __getattr__(self, key): if key[0].isupper(): return '' return AttributeError('Please install lasso') + lasso = MockLasso() diff --git a/src/authentic2/context_processors.py b/src/authentic2/context_processors.py index 885ad91f..1f3dbbbb 100644 --- a/src/authentic2/context_processors.py +++ b/src/authentic2/context_processors.py @@ -23,11 +23,12 @@ from .models import Service class UserFederations(object): '''Provide access to all federations of the current user''' + def __init__(self, request): self.request = request def __getattr__(self, name): - d = {'provider': None, 'links': [] } + d = {'provider': None, 'links': []} if name.startswith('service_'): try: provider_id = int(name.split('_', 1)[1]) @@ -43,6 +44,7 @@ class UserFederations(object): return d return super(UserFederations, self).__getattr__(name) + __AUTHENTIC2_DISTRIBUTION = None diff --git a/src/authentic2/cors.py b/src/authentic2/cors.py index 03c3d9f5..77d322fc 100644 --- a/src/authentic2/cors.py +++ b/src/authentic2/cors.py @@ -63,5 +63,3 @@ def check_origin(request, origin): if plugin.check_origin(request, origin): return True return False - - diff --git a/src/authentic2/crypto.py b/src/authentic2/crypto.py index d369bca6..ab361efb 100644 --- a/src/authentic2/crypto.py +++ b/src/authentic2/crypto.py @@ -54,9 +54,9 @@ def get_hashclass(name): def aes_base64_encrypt(key, data): - '''Generate an AES key from any key material using PBKDF2, and encrypt data using CFB mode. A - new IV is generated each time, the IV is also used as salt for PBKDF2. - ''' + """Generate an AES key from any key material using PBKDF2, and encrypt data using CFB mode. A + new IV is generated each time, the IV is also used as salt for PBKDF2. + """ iv = Random.get_random_bytes(16) aes_key = PBKDF2(key, iv) aes = AES.new(aes_key, AES.MODE_CFB, iv=iv) @@ -100,16 +100,15 @@ def add_padding(msg, block_size): def remove_padding(msg, block_size): '''Ignore padded zero bytes''' try: - msg_length, = struct.unpack(' len(msg) - 2: raise DecryptionError('wrong padding') - if len(msg[2 + msg_length:].strip(force_bytes('\0'))): + if len(msg[2 + msg_length :].strip(force_bytes('\0'))): raise DecryptionError('padding is not all zero') if len(unpadded) != msg_length: raise DecryptionError('wrong padding') @@ -117,11 +116,11 @@ def remove_padding(msg, block_size): def aes_base64url_deterministic_encrypt(key, data, salt, hash_name='sha256', count=1): - '''Encrypt using AES-128 and sign using HMAC-SHA256 shortened to 64 bits. + """Encrypt using AES-128 and sign using HMAC-SHA256 shortened to 64 bits. - Count and algorithm are encoded in the final string for future evolution. + Count and algorithm are encoded in the final string for future evolution. - ''' + """ mode = 1 # AES128-SHA256 hashmod = SHA256 key_size = 16 @@ -200,7 +199,11 @@ def hmac_url(key, url): key = key.encode('utf-8') if hasattr(url, 'isnumeric'): url = url.encode('utf-8', 'replace') - return base64.b32encode(hmac.HMAC(key=key, msg=url, digestmod=hashlib.sha256).digest()).decode('ascii').strip('=') + return ( + base64.b32encode(hmac.HMAC(key=key, msg=url, digestmod=hashlib.sha256).digest()) + .decode('ascii') + .strip('=') + ) def check_hmac_url(key, url, signature): diff --git a/src/authentic2/csv_import.py b/src/authentic2/csv_import.py index 47853634..ceac2194 100644 --- a/src/authentic2/csv_import.py +++ b/src/authentic2/csv_import.py @@ -257,23 +257,15 @@ SPECIAL_COLUMNS = SOURCE_COLUMNS | {ROLE_NAME, ROLE_SLUG, REGISTRATION, PASSWORD class ImportUserForm(BaseUserForm): - locals()[ROLE_NAME] = forms.CharField( - label=_('Role name'), - required=False) - locals()[ROLE_SLUG] = forms.CharField( - label=_('Role slug'), - required=False) + locals()[ROLE_NAME] = forms.CharField(label=_('Role name'), required=False) + locals()[ROLE_SLUG] = forms.CharField(label=_('Role slug'), required=False) choices = [ (REGISTRATION_RESET_EMAIL, _('Email user so they can set a password')), ] locals()[REGISTRATION] = forms.ChoiceField( - choices=choices, - label=_('Registration option'), - required=False) - locals()[PASSWORD_HASH] = forms.CharField( - label=_('Password hash'), - required=False) - + choices=choices, label=_('Registration option'), required=False + ) + locals()[PASSWORD_HASH] = forms.CharField(label=_('Password hash'), required=False) def clean(self): super(BaseUserForm, self).clean() @@ -296,9 +288,11 @@ class ImportUserFormWithExternalId(ImportUserForm): RegexValidator( r'^[a-zA-Z0-9_-]+$', _('_source_name must contain no spaces and only letters, digits, - and _'), - 'invalid')]) - locals()[SOURCE_ID] = forms.CharField( - label=_('Source external id')) + 'invalid', + ) + ], + ) + locals()[SOURCE_ID] = forms.CharField(label=_('Source external id')) @attrs @@ -410,11 +404,7 @@ class UserCsvImporter(object): except Simulate: pass - for action in [ - parse_csv, - self.parse_header_row, - self.parse_rows, - do_import]: + for action in [parse_csv, self.parse_header_row, self.parse_rows, do_import]: action() if self.errors: break @@ -454,20 +444,19 @@ class UserCsvImporter(object): header_names = set(self.headers_by_name) if header_names & SOURCE_COLUMNS and not SOURCE_COLUMNS.issubset(header_names): self.add_error( - Error('invalid-external-id-pair', - _('You must have a _source_name and a _source_id column'))) + Error('invalid-external-id-pair', _('You must have a _source_name and a _source_id column')) + ) if ROLE_NAME in header_names and ROLE_SLUG in header_names: self.add_error( - Error('invalid-role-column', - _('Either specify role names or role slugs, not both'))) + Error('invalid-role-column', _('Either specify role names or role slugs, not both')) + ) def parse_header(self, head, column): splitted = head.split() try: header = CsvHeader(column, splitted[0]) if header.name in self.headers_by_name: - self.add_error( - Error('duplicate-header', _('Header "%s" is duplicated') % header.name)) + self.add_error(Error('duplicate-header', _('Header "%s" is duplicated') % header.name)) return self.headers_by_name[header.name] = header except IndexError: @@ -503,19 +492,26 @@ class UserCsvImporter(object): self.headers.append(header) - if (not (header.field or header.attribute) - and header.name not in SPECIAL_COLUMNS): - self.add_error(LineError('unknown-or-missing-attribute', - _('unknown or missing attribute "%s"') % head, - line=1, column=column)) + if not (header.field or header.attribute) and header.name not in SPECIAL_COLUMNS: + self.add_error( + LineError( + 'unknown-or-missing-attribute', + _('unknown or missing attribute "%s"') % head, + line=1, + column=column, + ) + ) return for flag in splitted[1:]: if header.name in SOURCE_COLUMNS: - self.add_error(LineError( - 'flag-forbidden-on-source-columns', - _('You cannot set flags on _source_name and _source_id columns'), - line=1)) + self.add_error( + LineError( + 'flag-forbidden-on-source-columns', + _('You cannot set flags on _source_name and _source_id columns'), + line=1, + ) + ) break value = True if flag.startswith('no-'): @@ -537,7 +533,7 @@ class UserCsvImporter(object): rows = self.rows = [] for i, row in enumerate(self.csv_importer.rows[1:]): csv_row = self.parse_row(form_class, row, line=i + 2) - self.has_errors = self.has_errors or not(csv_row.is_valid) + self.has_errors = self.has_errors or not (csv_row.is_valid) rows.append(csv_row) def parse_row(self, form_class, row, line): @@ -561,15 +557,13 @@ class UserCsvImporter(object): header=header, value=form.cleaned_data.get(header.name), missing=header.name not in data, - errors=get_form_errors(form, header.name)) - for header in self.headers] + errors=get_form_errors(form, header.name), + ) + for header in self.headers + ] cell_errors = any(bool(cell.errors) for cell in cells) errors = get_form_errors(form, '__all__') - return CsvRow( - line=line, - cells=cells, - errors=errors, - is_valid=not bool(cell_errors or errors)) + return CsvRow(line=line, cells=cells, errors=errors, is_valid=not bool(cell_errors or errors)) @property def email_is_unique(self): @@ -611,16 +605,22 @@ class UserCsvImporter(object): row.user_first_seen = False else: errors.append( - Error('unique-constraint-failed', - _('Unique constraint on column "%(column)s" failed: ' - 'value already appear on line %(line)d') % { - 'column': header.name, - 'line': unique_map[unique_key]})) + Error( + 'unique-constraint-failed', + _( + 'Unique constraint on column "%(column)s" failed: ' + 'value already appear on line %(line)d' + ) + % {'column': header.name, 'line': unique_map[unique_key]}, + ) + ) else: unique_map[unique_key] = row.line for cell in row: - if (not cell.header.globally_unique and not cell.header.unique) or (user and not cell.header.update): + if (not cell.header.globally_unique and not cell.header.unique) or ( + user and not cell.header.update + ): continue if not cell.value: continue @@ -637,8 +637,11 @@ class UserCsvImporter(object): row.user_first_seen = False else: errors.append( - Error('unique-constraint-failed', - _('Unique constraint on column "%s" failed') % cell.header.name)) + Error( + 'unique-constraint-failed', + _('Unique constraint on column "%s" failed') % cell.header.name, + ) + ) row.errors.extend(errors) row.is_valid = row.is_valid and not bool(errors) return not bool(errors) @@ -680,8 +683,8 @@ class UserCsvImporter(object): if len(users) > 1: row.errors.append( - Error('key-matches-too-many-users', - _('Key value "%s" matches too many users') % key_value)) + Error('key-matches-too-many-users', _('Key value "%s" matches too many users') % key_value) + ) return False user = None @@ -701,7 +704,9 @@ class UserCsvImporter(object): for cell in row.cells: if not cell.header.field: continue - if (row.action == 'create' and cell.header.create) or (row.action == 'update' and cell.header.update): + if (row.action == 'create' and cell.header.create) or ( + row.action == 'update' and cell.header.update + ): if getattr(user, cell.header.name) != cell.value: setattr(user, cell.header.name, cell.value) if cell.header.name == 'email' and cell.header.verified: @@ -714,21 +719,21 @@ class UserCsvImporter(object): if header_key.name == SOURCE_ID and row.action == 'create': try: - UserExternalId.objects.create(user=user, - source=source_name, - external_id=source_id) + UserExternalId.objects.create(user=user, source=source_name, external_id=source_id) except IntegrityError: # should never happen since we have a unique index... source_full_id = '%s.%s' % (source_name, source_id) row.errors.append( - Error('external-id-already-exist', - _('External id "%s" already exists') % source_full_id)) + Error('external-id-already-exist', _('External id "%s" already exists') % source_full_id) + ) raise CancelImport for cell in row.cells: if cell.header.field or not cell.header.attribute: continue - if (row.action == 'create' and cell.header.create) or (row.action == 'update' and cell.header.update): + if (row.action == 'create' and cell.header.create) or ( + row.action == 'update' and cell.header.update + ): attributes = user.attributes if cell.header.verified: attributes = user.verified_attributes @@ -762,9 +767,7 @@ class UserCsvImporter(object): role = Role.objects.get(slug=cell.value, ou=self.ou) except Role.DoesNotExist: self._missing_roles.add(cell.value) - cell.errors.append( - Error('role-not-found', - _('Role "%s" does not exist') % cell.value)) + cell.errors.append(Error('role-not-found', _('Role "%s" does not exist') % cell.value)) return False if cell.header.delete: user.roles.remove(role) @@ -781,8 +784,11 @@ class UserCsvImporter(object): if cell.value == REGISTRATION_RESET_EMAIL: send_password_reset_mail( user, - template_names=['authentic2/manager/user_create_registration_email', - 'authentic2/password_reset'], + template_names=[ + 'authentic2/manager/user_create_registration_email', + 'authentic2/password_reset', + ], next_url='/accounts/', - context={'user': user}) + context={'user': user}, + ) return True diff --git a/src/authentic2/custom_user/apps.py b/src/authentic2/custom_user/apps.py index f0f888a2..8b8eec05 100644 --- a/src/authentic2/custom_user/apps.py +++ b/src/authentic2/custom_user/apps.py @@ -25,12 +25,11 @@ class CustomUserConfig(AppConfig): def ready(self): from django.db.models.signals import post_migrate - post_migrate.connect( - self.create_first_name_last_name_attributes, - sender=self) + post_migrate.connect(self.create_first_name_last_name_attributes, sender=self) - def create_first_name_last_name_attributes(self, app_config, verbosity=2, interactive=True, - using=DEFAULT_DB_ALIAS, **kwargs): + def create_first_name_last_name_attributes( + self, app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs + ): from django.utils import translation from django.utils.translation import ugettext_lazy as _ from django.conf import settings @@ -52,20 +51,26 @@ class CustomUserConfig(AppConfig): attrs = {} attrs['first_name'], created = Attribute.objects.get_or_create( name='first_name', - defaults={'kind': 'string', - 'label': _('First name'), - 'required': True, - 'asked_on_registration': True, - 'user_editable': True, - 'user_visible': True}) + defaults={ + 'kind': 'string', + 'label': _('First name'), + 'required': True, + 'asked_on_registration': True, + 'user_editable': True, + 'user_visible': True, + }, + ) attrs['last_name'], created = Attribute.objects.get_or_create( name='last_name', - defaults={'kind': 'string', - 'label': _('Last name'), - 'required': True, - 'asked_on_registration': True, - 'user_editable': True, - 'user_visible': True}) + defaults={ + 'kind': 'string', + 'label': _('Last name'), + 'required': True, + 'asked_on_registration': True, + 'user_editable': True, + 'user_visible': True, + }, + ) serialize = get_kind('string').get('serialize') for user in User.objects.all(): @@ -77,5 +82,6 @@ class CustomUserConfig(AppConfig): defaults={ 'multiple': False, 'verified': False, - 'content': serialize(getattr(user, attr_name, None)) - }) + 'content': serialize(getattr(user, attr_name, None)), + }, + ) diff --git a/src/authentic2/custom_user/management/commands/changepassword.py b/src/authentic2/custom_user/management/commands/changepassword.py index fb7fbfb6..be045e1d 100644 --- a/src/authentic2/custom_user/management/commands/changepassword.py +++ b/src/authentic2/custom_user/management/commands/changepassword.py @@ -34,8 +34,12 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('username', nargs='?', type=str) parser.add_argument( - '--database', action='store', dest='database', - default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".') + '--database', + action='store', + dest='database', + default=DEFAULT_DB_ALIAS, + help='Specifies the database to use. Default is "default".', + ) def _get_pass(self, prompt="Password: "): p = getpass.getpass(prompt=force_str(prompt)) diff --git a/src/authentic2/custom_user/management/commands/fix-attributes.py b/src/authentic2/custom_user/management/commands/fix-attributes.py index c6916dd4..d6f7c0aa 100644 --- a/src/authentic2/custom_user/management/commands/fix-attributes.py +++ b/src/authentic2/custom_user/management/commands/fix-attributes.py @@ -31,11 +31,10 @@ class Command(BaseCommand): i = 0 while True: - batch = user_ids[i * 100:i * 100 + 100] + batch = user_ids[i * 100 : i * 100 + 100] if not batch: break - users = User.objects.prefetch_related('attribute_values__attribute').filter( - id__in=batch) + users = User.objects.prefetch_related('attribute_values__attribute').filter(id__in=batch) count = 0 for user in users: try: @@ -70,5 +69,3 @@ class Command(BaseCommand): count += 1 i += 1 print('Fixed %d users.' % count) - - diff --git a/src/authentic2/custom_user/managers.py b/src/authentic2/custom_user/managers.py index b59daf1a..18331906 100644 --- a/src/authentic2/custom_user/managers.py +++ b/src/authentic2/custom_user/managers.py @@ -46,9 +46,7 @@ class UserQuerySet(models.QuerySet): if '@' in search and len(search.split()) == 1: with connection.cursor() as cursor: - cursor.execute( - "SET pg_trgm.similarity_threshold = %f" % app_settings.A2_FTS_THRESHOLD - ) + cursor.execute("SET pg_trgm.similarity_threshold = %f" % app_settings.A2_FTS_THRESHOLD) qs = self.filter(email__icontains=search).order_by(Unaccent('last_name'), Unaccent('first_name')) if qs.exists(): return wrap_qs(qs) @@ -74,7 +72,8 @@ class UserQuerySet(models.QuerySet): pass else: attribute_values = AttributeValue.objects.filter( - search_vector=SearchQuery(phone_number), attribute__kind='phone_number') + search_vector=SearchQuery(phone_number), attribute__kind='phone_number' + ) qs = self.filter(attribute_values__in=attribute_values).order_by('last_name', 'first_name') if qs.exists(): return wrap_qs(qs) @@ -85,27 +84,32 @@ class UserQuerySet(models.QuerySet): pass else: attribute_values = AttributeValue.objects.filter( - search_vector=SearchQuery(date.isoformat()), attribute__kind='birthdate') + search_vector=SearchQuery(date.isoformat()), attribute__kind='birthdate' + ) qs = self.filter(attribute_values__in=attribute_values).order_by('last_name', 'first_name') if qs.exists(): return wrap_qs(qs) qs = self.find_duplicates(fullname=search, limit=None, threshold=app_settings.A2_FTS_THRESHOLD) extra_user_ids = set() - attribute_values = AttributeValue.objects.filter(search_vector=SearchQuery(search), attribute__searchable=True) + attribute_values = AttributeValue.objects.filter( + search_vector=SearchQuery(search), attribute__searchable=True + ) extra_user_ids.update(self.filter(attribute_values__in=attribute_values).values_list('id', flat=True)) if len(search.split()) == 1: extra_user_ids.update( - self.filter( - Q(username__istartswith=search) - | Q(email__istartswith=search) - ).values_list('id', flat=True)) + self.filter(Q(username__istartswith=search) | Q(email__istartswith=search)).values_list( + 'id', flat=True + ) + ) if extra_user_ids: qs = qs | self.filter(id__in=extra_user_ids) qs = qs.order_by('dist', Unaccent('last_name'), Unaccent('first_name')) return qs - def find_duplicates(self, first_name=None, last_name=None, fullname=None, birthdate=None, limit=5, threshold=None): + def find_duplicates( + self, first_name=None, last_name=None, fullname=None, birthdate=None, limit=5, threshold=None + ): with connection.cursor() as cursor: cursor.execute( "SET pg_trgm.similarity_threshold = %f" % (threshold or app_settings.A2_DUPLICATES_THRESHOLD) @@ -133,20 +137,19 @@ class UserQuerySet(models.QuerySet): object_id=OuterRef('pk'), content_type=content_type, attribute__kind='birthdate', - content=birthdate + content=birthdate, ).annotate(bonus=Value(1 - bonus, output_field=FloatField())) - qs = qs.annotate(dist=Coalesce( - Subquery(same_birthdate.values('bonus'), output_field=FloatField()) * F('dist'), - F('dist') - )) + qs = qs.annotate( + dist=Coalesce( + Subquery(same_birthdate.values('bonus'), output_field=FloatField()) * F('dist'), F('dist') + ) + ) return qs class UserManager(BaseUserManager): - - def _create_user(self, username, email, password, - is_staff, is_superuser, **extra_fields): + def _create_user(self, username, email, password, is_staff, is_superuser, **extra_fields): """ Creates and saves a User with the given username, email and password. """ @@ -154,21 +157,25 @@ class UserManager(BaseUserManager): if not username: raise ValueError('The given username must be set') email = self.normalize_email(email) - user = self.model(username=username, email=email, - is_staff=is_staff, is_active=True, - is_superuser=is_superuser, last_login=now, - date_joined=now, **extra_fields) + user = self.model( + username=username, + email=email, + is_staff=is_staff, + is_active=True, + is_superuser=is_superuser, + last_login=now, + date_joined=now, + **extra_fields, + ) user.set_password(password) user.save(using=self._db) return user def create_user(self, username, email=None, password=None, **extra_fields): - return self._create_user(username, email, password, False, False, - **extra_fields) + return self._create_user(username, email, password, False, False, **extra_fields) def create_superuser(self, username, email, password, **extra_fields): - return self._create_user(username, email, password, True, True, - **extra_fields) + return self._create_user(username, email, password, True, True, **extra_fields) def get_by_natural_key(self, uuid): return self.get(uuid=uuid) diff --git a/src/authentic2/custom_user/migrations/0001_initial.py b/src/authentic2/custom_user/migrations/0001_initial.py index 1027dfb1..371a5e38 100644 --- a/src/authentic2/custom_user/migrations/0001_initial.py +++ b/src/authentic2/custom_user/migrations/0001_initial.py @@ -6,15 +6,27 @@ import django.utils.timezone import authentic2.utils import authentic2.validators + def noop(apps, schema_editor): pass + def copy_old_users_to_custom_user_model(apps, schema_editor): OldUser = apps.get_model('auth', 'User') NewUser = apps.get_model('custom_user', 'User') - fields = ['id', 'username', 'email', 'first_name', 'last_name', - 'is_staff', 'is_active', 'date_joined', 'is_superuser', - 'last_login', 'password'] + fields = [ + 'id', + 'username', + 'email', + 'first_name', + 'last_name', + 'is_staff', + 'is_active', + 'date_joined', + 'is_superuser', + 'last_login', + 'password', + ] old_users = OldUser.objects.prefetch_related('groups', 'user_permissions').order_by('id') new_users = [] for old_user in old_users: @@ -40,42 +52,123 @@ def copy_old_users_to_custom_user_model(apps, schema_editor): PermissionThrough.objects.bulk_create(new_permissions) # Reset sequences if schema_editor.connection.vendor == 'postgresql': - schema_editor.execute('SELECT setval(pg_get_serial_sequence(\'"custom_user_user_groups"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user_groups";') - schema_editor.execute('SELECT setval(pg_get_serial_sequence(\'"custom_user_user_user_permissions"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user_user_permissions";') - schema_editor.execute('SELECT setval(pg_get_serial_sequence(\'"custom_user_user"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user";') + schema_editor.execute( + 'SELECT setval(pg_get_serial_sequence(\'"custom_user_user_groups"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user_groups";' + ) + schema_editor.execute( + 'SELECT setval(pg_get_serial_sequence(\'"custom_user_user_user_permissions"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user_user_permissions";' + ) + schema_editor.execute( + 'SELECT setval(pg_get_serial_sequence(\'"custom_user_user"\',\'id\'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "custom_user_user";' + ) elif schema_editor.connection.vendor == 'sqlite': - schema_editor.execute('UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user) WHERE name = "custom_user_user";') - schema_editor.execute('UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user_groups) WHERE name = "custom_user_user_groups";') - schema_editor.execute('UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user_user_permissions) WHERE name = "custom_user_user_permissions";') + schema_editor.execute( + 'UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user) WHERE name = "custom_user_user";' + ) + schema_editor.execute( + 'UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user_groups) WHERE name = "custom_user_user_groups";' + ) + schema_editor.execute( + 'UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM custom_user_user_user_permissions) WHERE name = "custom_user_user_permissions";' + ) else: raise NotImplementedError() - class Migration(migrations.Migration): dependencies = [ - ('auth', '__first__'), + ('auth', '__first__'), ] operations = [ migrations.CreateModel( name='User', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(default=django.utils.timezone.now, 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')), - ('uuid', models.CharField(default=authentic2.utils.get_hex_uuid, verbose_name='uuid', unique=True, max_length=32, editable=False)), - ('username', models.CharField(max_length=256, null=True, verbose_name='username', blank=True)), + ( + 'last_login', + models.DateTimeField(default=django.utils.timezone.now, 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', + ), + ), + ( + 'uuid', + models.CharField( + default=authentic2.utils.get_hex_uuid, + verbose_name='uuid', + unique=True, + max_length=32, + editable=False, + ), + ), + ( + 'username', + models.CharField(max_length=256, null=True, verbose_name='username', blank=True), + ), ('first_name', models.CharField(max_length=64, verbose_name='first name', blank=True)), ('last_name', models.CharField(max_length=64, verbose_name='last name', blank=True)), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address', validators=[authentic2.validators.EmailValidator])), - ('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')), - ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), + ( + 'email', + models.EmailField( + blank=True, + max_length=254, + verbose_name='email address', + validators=[authentic2.validators.EmailValidator], + ), + ), + ( + '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'), + ), + ( + 'groups', + models.ManyToManyField( + related_query_name='user', + related_name='user_set', + to='auth.Group', + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', + verbose_name='groups', + ), + ), + ( + 'user_permissions', + models.ManyToManyField( + related_query_name='user', + related_name='user_set', + to='auth.Permission', + blank=True, + help_text='Specific permissions for this user.', + verbose_name='user permissions', + ), + ), ], options={ 'verbose_name': 'user', diff --git a/src/authentic2/custom_user/migrations/0002_auto_20150410_1823.py b/src/authentic2/custom_user/migrations/0002_auto_20150410_1823.py index 50178a32..fa275996 100644 --- a/src/authentic2/custom_user/migrations/0002_auto_20150410_1823.py +++ b/src/authentic2/custom_user/migrations/0002_auto_20150410_1823.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.conf import settings from django.db import models, migrations + class ThirdPartyAlterField(migrations.AlterField): def __init__(self, *args, **kwargs): self.app_label = kwargs.pop('app_label') @@ -15,19 +16,20 @@ class ThirdPartyAlterField(migrations.AlterField): def database_forwards(self, app_label, schema_editor, from_state, to_state): if hasattr(from_state, 'clear_delayed_apps_cache'): from_state.clear_delayed_apps_cache() - super(ThirdPartyAlterField, self).database_forwards(self.app_label, - schema_editor, from_state, to_state) + super(ThirdPartyAlterField, self).database_forwards( + self.app_label, schema_editor, from_state, to_state + ) def database_backwards(self, app_label, schema_editor, from_state, to_state): self.database_forwards(app_label, schema_editor, from_state, to_state) def __eq__(self, other): return ( - (self.__class__ == other.__class__) and - (self.app_label == other.app_label) and - (self.name == other.name) and - (self.model_name.lower() == other.model_name.lower()) and - (self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) + (self.__class__ == other.__class__) + and (self.app_label == other.app_label) + and (self.name == other.name) + and (self.model_name.lower() == other.model_name.lower()) + and (self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) ) def references_model(self, *args, **kwargs): @@ -48,12 +50,12 @@ class Migration(migrations.Migration): ] operations = [ - # Django admin log - ThirdPartyAlterField( - app_label='admin', - model_name='logentry', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), - preserve_default=True - ), + # Django admin log + ThirdPartyAlterField( + app_label='admin', + model_name='logentry', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + preserve_default=True, + ), ] diff --git a/src/authentic2/custom_user/migrations/0003_auto_20150504_1410.py b/src/authentic2/custom_user/migrations/0003_auto_20150504_1410.py index 209a4cf8..f1966513 100644 --- a/src/authentic2/custom_user/migrations/0003_auto_20150504_1410.py +++ b/src/authentic2/custom_user/migrations/0003_auto_20150504_1410.py @@ -13,6 +13,9 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='user', - options={'verbose_name': 'user', 'verbose_name_plural': 'users',}, + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, ), ] diff --git a/src/authentic2/custom_user/migrations/0004_user_ou.py b/src/authentic2/custom_user/migrations/0004_user_ou.py index bef7de66..6aab7d43 100644 --- a/src/authentic2/custom_user/migrations/0004_user_ou.py +++ b/src/authentic2/custom_user/migrations/0004_user_ou.py @@ -16,7 +16,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='ou', - field=models.ForeignKey(blank=True, to=settings.RBAC_OU_MODEL, null=True, on_delete=models.CASCADE), + field=models.ForeignKey( + blank=True, to=settings.RBAC_OU_MODEL, null=True, on_delete=models.CASCADE + ), preserve_default=True, ), ] diff --git a/src/authentic2/custom_user/migrations/0005_auto_20150522_1527.py b/src/authentic2/custom_user/migrations/0005_auto_20150522_1527.py index eb686db0..bcf37fa7 100644 --- a/src/authentic2/custom_user/migrations/0005_auto_20150522_1527.py +++ b/src/authentic2/custom_user/migrations/0005_auto_20150522_1527.py @@ -15,7 +15,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='ou', - field=models.ForeignKey(verbose_name='organizational unit', blank=True, to=settings.RBAC_OU_MODEL, null=True, on_delete=models.CASCADE), + field=models.ForeignKey( + verbose_name='organizational unit', + blank=True, + to=settings.RBAC_OU_MODEL, + null=True, + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/src/authentic2/custom_user/migrations/0006_auto_20150527_1212.py b/src/authentic2/custom_user/migrations/0006_auto_20150527_1212.py index e6a83b69..9ed54453 100644 --- a/src/authentic2/custom_user/migrations/0006_auto_20150527_1212.py +++ b/src/authentic2/custom_user/migrations/0006_auto_20150527_1212.py @@ -3,13 +3,15 @@ from __future__ import unicode_literals from django.db import models, migrations + def noop(apps, schema_editor): pass + def set_last_login(apps, schema_editor): User = apps.get_model('custom_user', 'User') - User.objects.filter(last_login__isnull=True) \ - .update(last_login=models.F('date_joined')) + User.objects.filter(last_login__isnull=True).update(last_login=models.F('date_joined')) + class Migration(migrations.Migration): diff --git a/src/authentic2/custom_user/migrations/0007_auto_20150610_1527.py b/src/authentic2/custom_user/migrations/0007_auto_20150610_1527.py index 6d10ddba..32d6a800 100644 --- a/src/authentic2/custom_user/migrations/0007_auto_20150610_1527.py +++ b/src/authentic2/custom_user/migrations/0007_auto_20150610_1527.py @@ -3,13 +3,15 @@ from __future__ import unicode_literals from django.db import models, migrations + def noop(apps, schema_editor): pass + def set_last_login(apps, schema_editor): User = apps.get_model('custom_user', 'User') - User.objects.filter(last_login__isnull=True) \ - .update(last_login=models.F('date_joined')) + User.objects.filter(last_login__isnull=True).update(last_login=models.F('date_joined')) + class Migration(migrations.Migration): diff --git a/src/authentic2/custom_user/migrations/0009_auto_20150810_1953.py b/src/authentic2/custom_user/migrations/0009_auto_20150810_1953.py index a7eab7bc..d36b8b51 100644 --- a/src/authentic2/custom_user/migrations/0009_auto_20150810_1953.py +++ b/src/authentic2/custom_user/migrations/0009_auto_20150810_1953.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='user', - options={'ordering': ('first_name', 'last_name', 'email', 'username'), 'verbose_name': 'user', 'verbose_name_plural': 'users',}, + options={ + 'ordering': ('first_name', 'last_name', 'email', 'username'), + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, ), ] diff --git a/src/authentic2/custom_user/migrations/0012_user_modified.py b/src/authentic2/custom_user/migrations/0012_user_modified.py index 4b1aa2e3..8ad2bac5 100644 --- a/src/authentic2/custom_user/migrations/0012_user_modified.py +++ b/src/authentic2/custom_user/migrations/0012_user_modified.py @@ -16,7 +16,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='modified', - field=models.DateTimeField(default=datetime.datetime(2017, 3, 13, 14, 41, 7, 593150, tzinfo=utc), auto_now=True, verbose_name='Last modification time', db_index=True), + field=models.DateTimeField( + default=datetime.datetime(2017, 3, 13, 14, 41, 7, 593150, tzinfo=utc), + auto_now=True, + verbose_name='Last modification time', + db_index=True, + ), preserve_default=False, ), ] diff --git a/src/authentic2/custom_user/migrations/0015_auto_20170707_1653.py b/src/authentic2/custom_user/migrations/0015_auto_20170707_1653.py index 197e44c6..ccc9953a 100644 --- a/src/authentic2/custom_user/migrations/0015_auto_20170707_1653.py +++ b/src/authentic2/custom_user/migrations/0015_auto_20170707_1653.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='user', - options={'ordering': ('last_name', 'first_name', 'email', 'username'), 'verbose_name': 'user', 'verbose_name_plural': 'users',}, + options={ + 'ordering': ('last_name', 'first_name', 'email', 'username'), + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, ), ] diff --git a/src/authentic2/custom_user/migrations/0017_auto_20200305_1645.py b/src/authentic2/custom_user/migrations/0017_auto_20200305_1645.py index 35e3a4be..76ebb37f 100644 --- a/src/authentic2/custom_user/migrations/0017_auto_20200305_1645.py +++ b/src/authentic2/custom_user/migrations/0017_auto_20200305_1645.py @@ -17,6 +17,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='email', - field=models.EmailField(blank=True, max_length=254, verbose_name='email address', validators=[authentic2.validators.email_validator]), + field=models.EmailField( + blank=True, + max_length=254, + verbose_name='email address', + validators=[authentic2.validators.email_validator], + ), ), ] diff --git a/src/authentic2/custom_user/migrations/0020_deleteduser.py b/src/authentic2/custom_user/migrations/0020_deleteduser.py index 7c9e679f..68dd9e1c 100644 --- a/src/authentic2/custom_user/migrations/0020_deleteduser.py +++ b/src/authentic2/custom_user/migrations/0020_deleteduser.py @@ -14,12 +14,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DeletedUser', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), ('deleted', models.DateTimeField(verbose_name='Deletion date', auto_now_add=True)), ('old_uuid', models.TextField(blank=True, null=True, verbose_name='Old UUID')), - ('old_user_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='Old user id')), - ('old_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Old email adress')), - ('old_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Old data')), + ( + 'old_user_id', + models.PositiveIntegerField(blank=True, null=True, verbose_name='Old user id'), + ), + ( + 'old_email', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='Old email adress'), + ), + ( + 'old_data', + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, null=True, verbose_name='Old data' + ), + ), ], options={ 'verbose_name': 'deleted user', diff --git a/src/authentic2/custom_user/migrations/0022_index_email.py b/src/authentic2/custom_user/migrations/0022_index_email.py index 55308e1e..797b069e 100644 --- a/src/authentic2/custom_user/migrations/0022_index_email.py +++ b/src/authentic2/custom_user/migrations/0022_index_email.py @@ -9,6 +9,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunSQL( sql=r'CREATE INDEX "custom_user_user_email_idx" ON "custom_user_user" (UPPER("email") text_pattern_ops);', - reverse_sql=r'DROP INDEX "custom_user_user_email_idx";' + reverse_sql=r'DROP INDEX "custom_user_user_email_idx";', ), ] diff --git a/src/authentic2/custom_user/migrations/0023_index_username.py b/src/authentic2/custom_user/migrations/0023_index_username.py index 97f8db7f..897befe3 100644 --- a/src/authentic2/custom_user/migrations/0023_index_username.py +++ b/src/authentic2/custom_user/migrations/0023_index_username.py @@ -9,6 +9,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunSQL( sql=r'CREATE INDEX "custom_user_user_username_idx" ON "custom_user_user" (UPPER("username") text_pattern_ops);', - reverse_sql=r'DROP INDEX "custom_user_user_username_idx";' + reverse_sql=r'DROP INDEX "custom_user_user_username_idx";', ), ] diff --git a/src/authentic2/custom_user/migrations/0024_index_email_by_trigrams.py b/src/authentic2/custom_user/migrations/0024_index_email_by_trigrams.py index ee7bcf66..e3b549c5 100644 --- a/src/authentic2/custom_user/migrations/0024_index_email_by_trigrams.py +++ b/src/authentic2/custom_user/migrations/0024_index_email_by_trigrams.py @@ -59,8 +59,10 @@ class Migration(migrations.Migration): operations = [ TrigramExtension(), RunSQLIfExtension( - sql=["CREATE INDEX IF NOT EXISTS custom_user_user_email_trgm_idx ON custom_user_user USING gist " - "(LOWER(email) public.gist_trgm_ops)"], + sql=[ + "CREATE INDEX IF NOT EXISTS custom_user_user_email_trgm_idx ON custom_user_user USING gist " + "(LOWER(email) public.gist_trgm_ops)" + ], reverse_sql=['DROP INDEX custom_user_user_email_trgm_idx'], ), ] diff --git a/src/authentic2/custom_user/migrations/0026_remove_user_deleted.py b/src/authentic2/custom_user/migrations/0026_remove_user_deleted.py index ba05bd97..dee7570f 100644 --- a/src/authentic2/custom_user/migrations/0026_remove_user_deleted.py +++ b/src/authentic2/custom_user/migrations/0026_remove_user_deleted.py @@ -13,8 +13,7 @@ def delete_users(apps, schema_editor): DeletedUser = apps.get_model('custom_user', 'DeletedUser') def delete_user(self): - deleted_user = DeletedUser( - old_user_id=self.id) + deleted_user = DeletedUser(old_user_id=self.id) if 'email' in app_settings.A2_USER_DELETED_KEEP_DATA: deleted_user.old_email = self.email.rsplit('#', 1)[0] if 'uuid' in app_settings.A2_USER_DELETED_KEEP_DATA: diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py index 33f137cc..7e39d771 100644 --- a/src/authentic2/custom_user/models.py +++ b/src/authentic2/custom_user/models.py @@ -28,6 +28,7 @@ from django.core.mail import send_mail from django.utils import six from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError, MultipleObjectsReturned + try: from django.contrib.contenttypes.fields import GenericRelation except ImportError: @@ -88,9 +89,8 @@ class Attributes(object): else: atv = self.values.get(name) self.values[name] = attribute.set_value( - self.owner, value, - verified=bool(self.verified), - attribute_value=atv) + self.owner, value, verified=bool(self.verified), attribute_value=atv + ) update_fields = ['modified'] if name in ['first_name', 'last_name']: @@ -128,10 +128,7 @@ class IsVerified(object): def __getattr__(self, name): v = getattr(self.user.attributes, name, None) - return ( - v is not None - and v == getattr(self.user.verified_attributes, name, None) - ) + return v is not None and v == getattr(self.user.verified_attributes, name, None) class IsVerifiedDescriptor(object): @@ -146,53 +143,42 @@ class User(AbstractBaseUser, PermissionMixin): Username, password and email are required. Other fields are optional. """ - uuid = models.CharField( - _('uuid'), - max_length=32, - default=utils.get_hex_uuid, editable=False, unique=True) + + uuid = models.CharField(_('uuid'), max_length=32, default=utils.get_hex_uuid, editable=False, unique=True) username = models.CharField(_('username'), max_length=256, null=True, blank=True) first_name = models.CharField(_('first name'), max_length=128, blank=True) last_name = models.CharField(_('last name'), max_length=128, blank=True) - email = models.EmailField( - _('email address'), - blank=True, - max_length=254, - validators=[email_validator]) - email_verified = models.BooleanField( - default=False, - verbose_name=_('email verified')) + email = models.EmailField(_('email address'), blank=True, max_length=254, validators=[email_validator]) + email_verified = models.BooleanField(default=False, verbose_name=_('email verified')) is_staff = models.BooleanField( _('staff status'), default=False, - help_text=_('Designates whether the user can log into this admin ' - 'site.')) + help_text=_('Designates whether the user can log into this admin ' 'site.'), + ) is_active = models.BooleanField( _('active'), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.')) + help_text=_( + 'Designates whether this user should be treated as ' + 'active. Unselect this instead of deleting accounts.' + ), + ) ou = models.ForeignKey( verbose_name=_('organizational unit'), to='a2_rbac.OrganizationalUnit', blank=True, null=True, swappable=False, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) # events dates date_joined = models.DateTimeField(_('date joined'), default=timezone.now) - modified = models.DateTimeField( - verbose_name=_('Last modification time'), - db_index=True, - auto_now=True) + modified = models.DateTimeField(verbose_name=_('Last modification time'), db_index=True, auto_now=True) last_account_deletion_alert = models.DateTimeField( - verbose_name=_('Last account deletion alert'), - null=True, - blank=True) - deactivation = models.DateTimeField( - verbose_name=_('Deactivation datetime'), - null=True, - blank=True) + verbose_name=_('Last account deletion alert'), null=True, blank=True + ) + deactivation = models.DateTimeField(verbose_name=_('Deactivation datetime'), null=True, blank=True) objects = UserManager.from_queryset(UserQuerySet)() attributes = AttributesDescriptor() @@ -237,10 +223,10 @@ class User(AbstractBaseUser, PermissionMixin): qs = (qs1 | qs2).order_by('name').distinct() RoleParenting = get_role_parenting_model() rp_qs = RoleParenting.objects.filter(child__in=qs1) - qs = qs.prefetch_related(models.Prefetch( - 'child_relation', queryset=rp_qs), 'child_relation__parent') - qs = qs.prefetch_related(models.Prefetch( - 'members', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='member')) + qs = qs.prefetch_related(models.Prefetch('child_relation', queryset=rp_qs), 'child_relation__parent') + qs = qs.prefetch_related( + models.Prefetch('members', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='member') + ) return qs def __str__(self): @@ -252,11 +238,13 @@ class User(AbstractBaseUser, PermissionMixin): return '' % six.text_type(self) def clean(self): - if not (self.username - or self.email - or (self.first_name and self.last_name)): - raise ValidationError(_('An account needs at least one identifier: ' - 'username, email or a full name (first and last name).')) + if not (self.username or self.email or (self.first_name and self.last_name)): + raise ValidationError( + _( + 'An account needs at least one identifier: ' + 'username, email or a full name (first and last name).' + ) + ) def validate_unique(self, exclude=None): errors = {} @@ -271,8 +259,11 @@ class User(AbstractBaseUser, PermissionMixin): if self.pk: qs = qs.exclude(pk=self.pk) - if 'username' not in exclude and self.username and (app_settings.A2_USERNAME_IS_UNIQUE - or (self.ou and self.ou.username_is_unique)): + if ( + 'username' not in exclude + and self.username + and (app_settings.A2_USERNAME_IS_UNIQUE or (self.ou and self.ou.username_is_unique)) + ): username_qs = qs if not app_settings.A2_USERNAME_IS_UNIQUE: username_qs = qs.filter(ou=self.ou) @@ -285,10 +276,14 @@ class User(AbstractBaseUser, PermissionMixin): pass else: errors.setdefault('username', []).append( - _('This username is already in use. Please supply a different username.')) + _('This username is already in use. Please supply a different username.') + ) - if 'email' not in exclude and self.email and (app_settings.A2_EMAIL_IS_UNIQUE - or (self.ou and self.ou.email_is_unique)): + if ( + 'email' not in exclude + and self.email + and (app_settings.A2_EMAIL_IS_UNIQUE or (self.ou and self.ou.email_is_unique)) + ): email_qs = qs if not app_settings.A2_EMAIL_IS_UNIQUE: email_qs = qs.filter(ou=self.ou) @@ -301,8 +296,8 @@ class User(AbstractBaseUser, PermissionMixin): pass else: errors.setdefault('email', []).append( - _('This email address is already in use. Please supply a different email ' - 'address.')) + _('This email address is already in use. Please supply a different email ' 'address.') + ) if errors: raise ValidationError(errors) @@ -319,20 +314,24 @@ class User(AbstractBaseUser, PermissionMixin): attribute = attributes_map[av.attribute_id] drf_field = attribute.get_drf_field() d[str(attribute.name)] = drf_field.to_representation(av.to_python()) - d.update({ - 'uuid': self.uuid, - 'username': self.username, - 'email': self.email, - 'ou': self.ou.name if self.ou else None, - 'ou__uuid': self.ou.uuid if self.ou else None, - 'ou__slug': self.ou.slug if self.ou else None, - 'ou__name': self.ou.name if self.ou else None, - 'first_name': self.first_name, - 'last_name': self.last_name, - 'is_superuser': self.is_superuser, - 'roles': [role.to_json() for role in self.roles_and_parents()], - 'services': [service.to_json(roles=self.roles_and_parents()) for service in Service.objects.all()], - }) + d.update( + { + 'uuid': self.uuid, + 'username': self.username, + 'email': self.email, + 'ou': self.ou.name if self.ou else None, + 'ou__uuid': self.ou.uuid if self.ou else None, + 'ou__slug': self.ou.slug if self.ou else None, + 'ou__name': self.ou.name if self.ou else None, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'is_superuser': self.is_superuser, + 'roles': [role.to_json() for role in self.roles_and_parents()], + 'services': [ + service.to_json(roles=self.roles_and_parents()) for service in Service.objects.all() + ], + } + ) return d def save(self, *args, **kwargs): @@ -373,8 +372,7 @@ class User(AbstractBaseUser, PermissionMixin): @transaction.atomic def delete(self, **kwargs): - deleted_user = DeletedUser( - old_user_id=self.id) + deleted_user = DeletedUser(old_user_id=self.id) if 'email' in app_settings.A2_USER_DELETED_KEEP_DATA: deleted_user.old_email = self.email.rsplit('#', 1)[0] if 'uuid' in app_settings.A2_USER_DELETED_KEEP_DATA: @@ -397,36 +395,25 @@ class User(AbstractBaseUser, PermissionMixin): class DeletedUser(models.Model): - deleted = models.DateTimeField( - verbose_name=_('Deletion date'), - auto_now_add=True) - old_uuid = models.TextField( - verbose_name=_('Old UUID'), - null=True, - blank=True) - old_user_id = models.PositiveIntegerField( - verbose_name=_('Old user id'), - null=True, - blank=True) - old_email = models.EmailField( - verbose_name=_('Old email adress'), - null=True, - blank=True) - old_data = JSONField( - verbose_name=_('Old data'), - null=True, - blank=True) + deleted = models.DateTimeField(verbose_name=_('Deletion date'), auto_now_add=True) + old_uuid = models.TextField(verbose_name=_('Old UUID'), null=True, blank=True) + old_user_id = models.PositiveIntegerField(verbose_name=_('Old user id'), null=True, blank=True) + old_email = models.EmailField(verbose_name=_('Old email adress'), null=True, blank=True) + old_data = JSONField(verbose_name=_('Old data'), null=True, blank=True) @classmethod def cleanup(cls, threshold=None, timestamp=None): - threshold = threshold or (timezone.now() - datetime.timedelta(days=app_settings.A2_USER_DELETED_KEEP_DATA_DAYS)) + threshold = threshold or ( + timezone.now() - datetime.timedelta(days=app_settings.A2_USER_DELETED_KEEP_DATA_DAYS) + ) cls.objects.filter(deleted__lt=threshold).delete() def __str__(self): return 'DeletedUser(old_id=%s, old_uuid=%s…, old_email=%s)' % ( self.old_user_id or '-', (self.old_uuid or '')[:6], - self.old_email or '-') + self.old_email or '-', + ) class Meta: verbose_name = _('deleted user') diff --git a/src/authentic2/data_transfer.py b/src/authentic2/data_transfer.py index 3e77587e..b8c48bda 100644 --- a/src/authentic2/data_transfer.py +++ b/src/authentic2/data_transfer.py @@ -24,8 +24,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.text import format_lazy from django_rbac.models import Operation -from django_rbac.utils import ( - get_ou_model, get_role_model, get_role_parenting_model, get_permission_model) +from django_rbac.utils import get_ou_model, get_role_model, get_role_parenting_model, get_permission_model from authentic2.decorators import errorcollector from authentic2.a2_rbac.models import RoleAttribute @@ -57,8 +56,13 @@ def update_model(obj, d): yield message.message else: yield message + for message in error_list(messages): - errorlist.append(format_lazy(u'{}="{}": {}', obj.__class__._meta.get_field(key).verbose_name, value, message)) + errorlist.append( + format_lazy( + u'{}="{}": {}', obj.__class__._meta.get_field(key).verbose_name, value, message + ) + ) raise ValidationError(errorlist) obj.save() @@ -99,12 +103,8 @@ def export_ous(context): def export_roles(context): - """ Serialize roles in role_queryset - """ - return [ - role.export_json(attributes=True, parents=True, permissions=True) - for role in context.role_qs - ] + """Serialize roles in role_queryset""" + return [role.export_json(attributes=True, parents=True, permissions=True) for role in context.role_qs] def search_ou(ou_d): @@ -129,9 +129,8 @@ def search_role(role_d, ou=None): return role - class ImportContext(object): - """ Holds information on how to perform the import. + """Holds information on how to perform the import. ou_delete_orphans: if True any existing ou that is not found in the export will be deleted @@ -152,15 +151,16 @@ class ImportContext(object): """ def __init__( - self, - import_roles=True, - import_ous=True, - role_delete_orphans=False, - role_parentings_update=True, - role_permissions_update=True, - role_attributes_update=True, - ou_delete_orphans=False, - set_ou=None): + self, + import_roles=True, + import_ous=True, + role_delete_orphans=False, + role_parentings_update=True, + role_permissions_update=True, + role_attributes_update=True, + ou_delete_orphans=False, + set_ou=None, + ): self.import_roles = import_roles self.import_ous = import_ous self.role_delete_orphans = role_delete_orphans @@ -196,10 +196,14 @@ class RoleDeserializer(object): try: return func(self, *args, **kwargs) except ValidationError as e: - raise ValidationError(_('Role "%(name)s": %(errors)s') % { - 'name': self._role_d.get('name', self._role_d.get('slug')), - 'errors': lazy_join(', ', [v.message for v in e.error_list]), - }) + raise ValidationError( + _('Role "%(name)s": %(errors)s') + % { + 'name': self._role_d.get('name', self._role_d.get('slug')), + 'errors': lazy_join(', ', [v.message for v in e.error_list]), + } + ) + return f @wraps_validationerror @@ -241,8 +245,7 @@ class RoleDeserializer(object): @wraps_validationerror def attributes(self): - """ Update attributes (delete everything then create) - """ + """Update attributes (delete everything then create)""" created, deleted = [], [] for attr in self._obj.attributes.all(): attr.delete() @@ -257,8 +260,7 @@ class RoleDeserializer(object): @wraps_validationerror def parentings(self): - """ Update parentings (delete everything then create) - """ + """Update parentings (delete everything then create)""" created, deleted = [], [] Parenting = get_role_parenting_model() for parenting in Parenting.objects.filter(child=self._obj, direct=True): @@ -270,15 +272,13 @@ class RoleDeserializer(object): parent = search_role(parent_d) if not parent: raise ValidationError(_("Could not find parent role: %s") % parent_d) - created.append(Parenting.objects.create( - child=self._obj, direct=True, parent=parent)) + created.append(Parenting.objects.create(child=self._obj, direct=True, parent=parent)) return created, deleted @wraps_validationerror def permissions(self): - """ Update permissions (delete everything then create) - """ + """Update permissions (delete everything then create)""" created, deleted = [], [] for perm in self._obj.permissions.all(): perm.delete() @@ -287,12 +287,12 @@ class RoleDeserializer(object): if self._permissions: for perm in self._permissions: op = Operation.objects.get_by_natural_key_json(perm['operation']) - ou = get_ou_model().objects.get_by_natural_key_json( - perm['ou']) if perm['ou'] else None + ou = get_ou_model().objects.get_by_natural_key_json(perm['ou']) if perm['ou'] else None ct = ContentType.objects.get_by_natural_key_json(perm['target_ct']) target = ct.model_class().objects.get_by_natural_key_json(perm['target']) perm = get_permission_model().objects.create( - operation=op, ou=ou, target_ct=ct, target_id=target.pk) + operation=op, ou=ou, target_ct=ct, target_id=target.pk + ) self._obj.permissions.add(perm) created.append(perm) @@ -300,7 +300,6 @@ class RoleDeserializer(object): class ImportResult(object): - def __init__(self): self.roles = {'created': [], 'updated': []} self.ous = {'created': [], 'updated': []} @@ -388,12 +387,15 @@ def import_site(json_d, import_context=None): result.update_permissions(*ds.permissions()) if import_context.ou_delete_orphans: - raise ValidationError(_("Unsupported context value for ou_delete_orphans : %s") % ( - import_context.ou_delete_orphans)) + raise ValidationError( + _("Unsupported context value for ou_delete_orphans : %s") % (import_context.ou_delete_orphans) + ) if import_context.role_delete_orphans: # FIXME : delete each role that is in DB but not in the export - raise ValidationError(_("Unsupported context value for role_delete_orphans : %s") % ( - import_context.role_delete_orphans)) + raise ValidationError( + _("Unsupported context value for role_delete_orphans : %s") + % (import_context.role_delete_orphans) + ) return result diff --git a/src/authentic2/decorators.py b/src/authentic2/decorators.py index 0eb3235b..d9ec93ae 100644 --- a/src/authentic2/decorators.py +++ b/src/authentic2/decorators.py @@ -29,6 +29,7 @@ from django.core.exceptions import ValidationError from django.utils import six from . import app_settings, middleware + # XXX: import to_list for retrocompaibility from .utils import to_list, to_iter # noqa: F401 @@ -39,13 +40,16 @@ class CacheUnusable(RuntimeError): def unless(test, message): '''Decorator returning a 404 status code if some condition is not met''' + def decorator(func): @wraps(func) def f(request, *args, **kwargs): if not test(): return technical_404_response(request, Http404(message)) return func(request, *args, **kwargs) + return f + return decorator @@ -55,6 +59,7 @@ def setting_enabled(name, settings=app_settings): def test(): return getattr(settings, name, False) + return unless(test, 'please enable %s' % full_name) @@ -62,14 +67,16 @@ def lasso_required(): def test(): try: import lasso # noqa: F401 + return True except ImportError: return False + return unless(test, 'please install lasso') def required(wrapping_functions, patterns_rslt): - ''' + """ Used to require 1..n decorators in any view returned by a url tree Usage: @@ -77,8 +84,8 @@ def required(wrapping_functions, patterns_rslt): urlpatterns = required((func,func,func),patterns(...)) Note: - Use functools.partial to pass keyword params to the required - decorators. If you need to pass args you will have to write a + Use functools.partial to pass keyword params to the required + decorators. If you need to pass args you will have to write a wrapper function. Example: @@ -88,14 +95,11 @@ def required(wrapping_functions, patterns_rslt): partial(login_required,login_url='/accounts/login/'), patterns(...) ) - ''' + """ if not hasattr(wrapping_functions, '__iter__'): wrapping_functions = (wrapping_functions,) - return [ - _wrap_instance__resolve(wrapping_functions, instance) - for instance in patterns_rslt - ] + return [_wrap_instance__resolve(wrapping_functions, instance) for instance in patterns_rslt] def _wrap_instance__resolve(wrapping_functions, instance): @@ -123,21 +127,23 @@ def _wrap_instance__resolve(wrapping_functions, instance): class CacheDecoratorBase(object): - '''Base class to build cache decorators. + """Base class to build cache decorators. + + It helps for building keys from function arguments. + """ - It helps for building keys from function arguments. - ''' def __new__(cls, *args, **kwargs): if len(args) > 1: raise TypeError( - '%s got unexpected arguments, only one argument must be given, the function to decorate' % cls.__name__) + '%s got unexpected arguments, only one argument must be given, the function to decorate' + % cls.__name__ + ) if args: # Case of a decorator used directly return cls(**kwargs)(args[0]) return super(CacheDecoratorBase, cls).__new__(cls) - def __init__(self, timeout=None, hostname_vary=True, args=None, - kwargs=None): + def __init__(self, timeout=None, hostname_vary=True, args=None, kwargs=None): self.timeout = timeout self.hostname_vary = hostname_vary self.args = args @@ -162,8 +168,7 @@ class CacheDecoratorBase(object): key = self.key(*args, **kwargs) value, tstamp = self.get(key) if tstamp is not None: - if (self.timeout is None - or tstamp + self.timeout > now): + if self.timeout is None or tstamp + self.timeout > now: return value if hasattr(self, 'delete'): self.delete(key, (key, tstamp)) @@ -172,6 +177,7 @@ class CacheDecoratorBase(object): return value except CacheUnusable: # fallback when cache cannot be used return func(*args, **kwargs) + f.cache = self return f @@ -199,10 +205,11 @@ class CacheDecoratorBase(object): class SimpleDictionnaryCacheMixin(object): - '''Default implementations of set, get and delete for a cache implemented - using a dictionary. The dictionnary must be returned by a property named - 'cache'. - ''' + """Default implementations of set, get and delete for a cache implemented + using a dictionary. The dictionnary must be returned by a property named + 'cache'. + """ + def set(self, key, value): self.cache[key] = value @@ -264,8 +271,7 @@ class PickleCacheMixin(object): return value -class SessionCache(PickleCacheMixin, SimpleDictionnaryCacheMixin, - CacheDecoratorBase): +class SessionCache(PickleCacheMixin, SimpleDictionnaryCacheMixin, CacheDecoratorBase): @property def cache(self): request = middleware.StoreRequestMiddleware.get_request() @@ -308,7 +314,9 @@ def json(func): if variable in request.GET: identifier = request.GET[variable] if not re.match(r'^[$a-zA-Z_][0-9a-zA-Z_$]*$', identifier): - return HttpResponseBadRequest('invalid JSONP callback name', content_type='text/plain') + return HttpResponseBadRequest( + 'invalid JSONP callback name', content_type='text/plain' + ) jsonp = True break # 1. check origin @@ -336,4 +344,5 @@ def json(func): response['Access-Control-Allow-Headers'] = 'x-requested-with' response.write(json_str) return response + return f diff --git a/src/authentic2/disco_service/disco_responder.py b/src/authentic2/disco_service/disco_responder.py index f99c0e21..8a6e46a9 100644 --- a/src/authentic2/disco_service/disco_responder.py +++ b/src/authentic2/disco_service/disco_responder.py @@ -82,8 +82,9 @@ def get_disco_return_url_from_metadata(entity_id): logger.warn('get_disco_return_url_from_metadata: unknown service provider %s', entity_id) return None dom = parseString(liberty_provider.metadata.encode('utf8')) - endpoints = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', - 'DiscoveryResponse') + endpoints = dom.getElementsByTagNameNS( + 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', 'DiscoveryResponse' + ) if not endpoints: logger.warn('get_disco_return_url_from_metadata: no discovery service endpoint for %s', entity_id) return None @@ -141,8 +142,7 @@ def disco(request): entityID = None _return = None - policy = \ - "urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single", + policy = ("urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single",) returnIDParam = None isPassive = False @@ -167,7 +167,9 @@ def disco(request): # Discovery request parameters entityID = request.GET.get('entityID', '') _return = request.GET.get('return', '') - policy = request.GET.get('idp_selected', 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single') + policy = request.GET.get( + 'idp_selected', 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single' + ) returnIDParam = request.GET.get('returnIDParam', 'entityID') # XXX: isPassive is unused isPassive = request.GET.get('isPassive', '') @@ -199,7 +201,8 @@ def disco(request): # equal to returnIDParam. Else, it is an unconformant SP. if is_param_id_in_return_url(return_url, returnIDParam): message = _('invalid return url %(return_url)s for %(entity_id)s') % dict( - return_url=return_url, entity_id=entityID) + return_url=return_url, entity_id=entityID + ) return error_page(request, message, logger=logger) # not back from selection interface @@ -227,6 +230,7 @@ def idp_selection(request): idp_selected = urlquote('http://www.identity-hub.com/idp/saml2/metadata') return HttpResponseRedirect('%s?idp_selected=%s' % (reverse(disco), idp_selected)) + urlpatterns = [ url(r'^disco$', disco), url(r'^idp_selection$', idp_selection), diff --git a/src/authentic2/exponential_retry_timeout.py b/src/authentic2/exponential_retry_timeout.py index 65bf8ac9..0968b8f2 100644 --- a/src/authentic2/exponential_retry_timeout.py +++ b/src/authentic2/exponential_retry_timeout.py @@ -29,12 +29,14 @@ class ExponentialRetryTimeout(object): KEY_PREFIX = 'exp-backoff-' CACHE_DURATION = 86400 - def __init__(self, - factor=FACTOR, - duration=DURATION, - max_duration=MAX_DURATION, - key_prefix=None, - cache_duration=CACHE_DURATION): + def __init__( + self, + factor=FACTOR, + duration=DURATION, + max_duration=MAX_DURATION, + key_prefix=None, + cache_duration=CACHE_DURATION, + ): self.factor = factor self.duration = duration self.max_duration = max_duration @@ -48,9 +50,9 @@ class ExponentialRetryTimeout(object): return '%s%s' % (self.key_prefix or self.KEY_PREFIX, hashlib.md5(key).hexdigest()) def seconds_to_wait(self, *keys): - '''Return the duration in seconds until the next time when an action can be - done. - ''' + """Return the duration in seconds until the next time when an action can be + done. + """ key = self.key(keys) if self.duration: now = time.time() @@ -60,8 +62,7 @@ class ExponentialRetryTimeout(object): return 0 def success(self, *keys): - '''Signal an action success, delete exponential backoff cache. - ''' + """Signal an action success, delete exponential backoff cache.""" key = self.key(keys) if not self.duration: return @@ -69,8 +70,7 @@ class ExponentialRetryTimeout(object): self.logger.debug(u'success for %s', keys) def failure(self, *keys): - '''Signal an action failure, augment the exponential backoff one level. - ''' + """Signal an action failure, augment the exponential backoff one level.""" key = self.key(keys) if not self.duration: return diff --git a/src/authentic2/forms/authentication.py b/src/authentic2/forms/authentication.py index d970134a..ab4a5bc0 100644 --- a/src/authentic2/forms/authentication.py +++ b/src/authentic2/forms/authentication.py @@ -39,11 +39,13 @@ class AuthenticationForm(auth_forms.AuthenticationForm): initial=False, required=False, label=_('Remember me'), - help_text=_('Do not ask for authentication next time')) + help_text=_('Do not ask for authentication next time'), + ) ou = forms.ModelChoiceField( label=lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL), required=True, - queryset=OU.objects.all()) + queryset=OU.objects.all(), + ) def __init__(self, *args, **kwargs): preferred_ous = kwargs.pop('preferred_ous', []) @@ -53,7 +55,8 @@ class AuthenticationForm(auth_forms.AuthenticationForm): self.exponential_backoff = ExponentialRetryTimeout( key_prefix='login-exp-backoff-', duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION, - factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR) + factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR, + ) if not app_settings.A2_USER_REMEMBER_ME: del self.fields['remember_me'] @@ -64,8 +67,7 @@ class AuthenticationForm(auth_forms.AuthenticationForm): if preferred_ous: choices = self.fields['ou'].choices new_choices = list(choices)[:1] + [ - (ugettext('Preferred organizational units'), [ - (ou.pk, ou.name) for ou in preferred_ous]), + (ugettext('Preferred organizational units'), [(ou.pk, ou.name) for ou in preferred_ous]), (ugettext('All organizational units'), list(choices)[1:]), ] self.fields['ou'].choices = new_choices @@ -88,9 +90,11 @@ class AuthenticationForm(auth_forms.AuthenticationForm): seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys) if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION: seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION - msg = _('You made too many login errors recently, you must ' - 'wait %s seconds ' - 'to try again.') + msg = _( + 'You made too many login errors recently, you must ' + 'wait %s seconds ' + 'to try again.' + ) msg = msg % int(math.ceil(seconds_to_wait)) msg = html.mark_safe(msg) raise forms.ValidationError(msg) @@ -140,16 +144,17 @@ class AuthenticationForm(auth_forms.AuthenticationForm): if app_settings.A2_USERNAME_LABEL: username_label = app_settings.A2_USERNAME_LABEL invalid_login_message = [ - _('Incorrect %(username_label)s or password.') % {'username_label': username_label}, + _('Incorrect %(username_label)s or password.') % {'username_label': username_label}, ] - if app_settings.A2_USER_CAN_RESET_PASSWORD is not False and getattr(settings, 'REGISTRATION_OPEN', True): + if app_settings.A2_USER_CAN_RESET_PASSWORD is not False and getattr( + settings, 'REGISTRATION_OPEN', True + ): invalid_login_message.append( - _('Try again, use the forgotten password link below, or create an account.')) + _('Try again, use the forgotten password link below, or create an account.') + ) elif app_settings.A2_USER_CAN_RESET_PASSWORD is not False: - invalid_login_message.append( - _('Try again or use the forgotten password link below.')) + invalid_login_message.append(_('Try again or use the forgotten password link below.')) elif getattr(settings, 'REGISTRATION_OPEN', True): - invalid_login_message.append( - _('Try again or create an account.')) + invalid_login_message.append(_('Try again or create an account.')) error_messages['invalid_login'] = ' '.join([force_text(x) for x in invalid_login_message]) return error_messages diff --git a/src/authentic2/forms/fields.py b/src/authentic2/forms/fields.py index 286384bd..13fdb534 100644 --- a/src/authentic2/forms/fields.py +++ b/src/authentic2/forms/fields.py @@ -24,9 +24,13 @@ from django.core.files import File from authentic2 import app_settings from authentic2.passwords import password_help_text, validate_password -from authentic2.forms.widgets import (PasswordInput, NewPasswordInput, - CheckPasswordInput, ProfileImageInput, - EmailInput) +from authentic2.forms.widgets import ( + PasswordInput, + NewPasswordInput, + CheckPasswordInput, + ProfileImageInput, + EmailInput, +) from authentic2.validators import email_validator import PIL.Image @@ -49,7 +53,9 @@ class CheckPasswordField(CharField): widget = CheckPasswordInput def __init__(self, *args, **kwargs): - kwargs['help_text'] = u''' + kwargs[ + 'help_text' + ] = u''' %(default)s %(match)s %(nomatch)s @@ -85,11 +91,7 @@ class ProfileImageField(FileField): output = io.BytesIO() if image.mode != 'RGB': image = image.convert('RGB') - image.save( - output, - format='JPEG', - quality=99, - optimize=1) + image.save(output, format='JPEG', quality=99, optimize=1) output.seek(0) return File(output, name=name) diff --git a/src/authentic2/forms/mixins.py b/src/authentic2/forms/mixins.py index 79abbf62..fb86ec65 100644 --- a/src/authentic2/forms/mixins.py +++ b/src/authentic2/forms/mixins.py @@ -61,7 +61,8 @@ class LockedFieldFormMixin(object): help_text=field.help_text, initial=initial, required=False, - widget=forms.TextInput(attrs={'readonly': ''})) + widget=forms.TextInput(attrs={'readonly': ''}), + ) if not locked_fields: return diff --git a/src/authentic2/forms/passwords.py b/src/authentic2/forms/passwords.py index 8d62b400..f71ab7b7 100644 --- a/src/authentic2/forms/passwords.py +++ b/src/authentic2/forms/passwords.py @@ -37,8 +37,7 @@ logger = logging.getLogger(__name__) class PasswordResetForm(forms.Form): next_url = forms.CharField(widget=forms.HiddenInput, required=False) - email = forms.CharField( - label=_("Email"), max_length=254) + email = forms.CharField(label=_("Email"), max_length=254) def save(self): """ @@ -51,12 +50,10 @@ class PasswordResetForm(forms.Form): for user in active_users: # we don't set the password to a random string, as some users should not have # a password - set_random_password = (user.has_usable_password() - and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET) + set_random_password = user.has_usable_password() and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET utils.send_password_reset_mail( - user, - set_random_password=set_random_password, - next_url=self.cleaned_data.get('next_url')) + user, set_random_password=set_random_password, next_url=self.cleaned_data.get('next_url') + ) for user in users.filter(is_active=False): logger.info('password reset failed for user "%r": account is disabled', user) utils.send_templated_mail(user, ['authentic2/password_reset_refused']) @@ -68,8 +65,8 @@ class PasswordResetForm(forms.Form): class PasswordResetMixin(Form): - '''Remove all password reset object for the current user when password is - successfully changed.''' + """Remove all password reset object for the current user when password is + successfully changed.""" def save(self, commit=True): ret = super(PasswordResetMixin, self).save(commit=commit) @@ -82,6 +79,7 @@ class PasswordResetMixin(Form): ret = old_save(*args, **kwargs) models.PasswordReset.objects.filter(user=self.user).delete() return ret + self.user.save = save return ret @@ -109,8 +107,9 @@ class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.Set return new_password1 -class PasswordChangeForm(NotifyOfPasswordChange, NextUrlFormMixin, PasswordResetMixin, - auth_forms.PasswordChangeForm): +class PasswordChangeForm( + NotifyOfPasswordChange, NextUrlFormMixin, PasswordResetMixin, auth_forms.PasswordChangeForm +): old_password = PasswordField(label=_('Old password')) new_password1 = NewPasswordField(label=_('New password')) new_password2 = CheckPasswordField(label=_("New password confirmation")) @@ -122,6 +121,7 @@ class PasswordChangeForm(NotifyOfPasswordChange, NextUrlFormMixin, PasswordReset raise ValidationError(_('New password must differ from old password')) return new_password1 + # make old_password the first field new_base_fields = OrderedDict() diff --git a/src/authentic2/forms/profile.py b/src/authentic2/forms/profile.py index e3f37701..f6f77ed0 100644 --- a/src/authentic2/forms/profile.py +++ b/src/authentic2/forms/profile.py @@ -50,8 +50,7 @@ class EmailChangeFormNoPassword(forms.Form): class EmailChangeForm(EmailChangeFormNoPassword): - password = forms.CharField(label=_("Password"), - widget=forms.PasswordInput) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) def clean_email(self): email = self.cleaned_data['email'] @@ -120,6 +119,7 @@ class BaseUserForm(LockedFieldFormMixin, forms.ModelForm): def save_m2m(*args, **kwargs): old(*args, **kwargs) self.save_attributes() + self.save_m2m = save_m2m return result @@ -129,10 +129,10 @@ class EditProfileForm(NextUrlFormMixin, BaseUserForm): def modelform_factory(model, **kwargs): - '''Build a modelform for the given model, + """Build a modelform for the given model, - For the user model also add attribute based fields. - ''' + For the user model also add attribute based fields. + """ form = kwargs.pop('form', None) fields = kwargs.get('fields') or [] @@ -159,15 +159,15 @@ def modelform_factory(model, **kwargs): form = forms.ModelForm modelform = None if required: + def __init__(self, *args, **kwargs): super(modelform, self).__init__(*args, **kwargs) for field in required: if field in self.fields: self.fields[field].required = True + d['__init__'] = __init__ modelform = type(model.__name__ + 'ModelForm', (form,), d) kwargs['form'] = modelform modelform.required_css_class = 'form-field-required' return dj_modelform_factory(model, **kwargs) - - diff --git a/src/authentic2/forms/registration.py b/src/authentic2/forms/registration.py index 48bc8972..263a7a89 100644 --- a/src/authentic2/forms/registration.py +++ b/src/authentic2/forms/registration.py @@ -100,8 +100,9 @@ class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm): else: exist = True if exist: - raise ValidationError(_('This username is already in ' - 'use. Please supply a different username.')) + raise ValidationError( + _('This username is already in ' 'use. Please supply a different username.') + ) return username def clean_email(self): @@ -118,8 +119,9 @@ class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm): else: exist = True if exist: - raise ValidationError(_('This email address is already in ' - 'use. Please supply a different email address.')) + raise ValidationError( + _('This email address is already in ' 'use. Please supply a different email address.') + ) return BaseUserManager.normalize_email(email) def save(self, commit=True): diff --git a/src/authentic2/forms/widgets.py b/src/authentic2/forms/widgets.py index d20478aa..9ce92e69 100644 --- a/src/authentic2/forms/widgets.py +++ b/src/authentic2/forms/widgets.py @@ -66,7 +66,7 @@ DATE_FORMAT_PY_JS_MAPPING = { '%Y': 'yyyy', '%y': 'yy', '%p': 'P', - '%S': 'ss' + '%S': 'ss', } DATE_FORMAT_TO_JS_REGEX = re.compile(r'(?' % self.name for element in self.data: datalist += '