misc: apply black (#52457)
This commit is contained in:
parent
57ded4fd8f
commit
4bb33d3d3c
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
118
doc/conf.py
118
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
|
||||
# "<project> v<release> 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 <link> 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.
|
||||
|
|
|
@ -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")
|
||||
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.*")
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
# write result to output file
|
||||
xml1.write(outputfile, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
# merge packages
|
||||
merge (packages1root, packages1, packages2, 'name', merge_packages);
|
||||
def filter_xml(xmlfile):
|
||||
xmlroot = xmlfile.getroot()
|
||||
packageroot = xmlfile.find(PACKAGES_ROOT)
|
||||
packages = xmlroot.findall(PACKAGES_LIST)
|
||||
|
||||
# write result to output file
|
||||
xml1.write (outputfile, encoding="UTF-8", xml_declaration=True)
|
||||
# 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 filter_xml (xmlfile):
|
||||
xmlroot = xmlfile.getroot()
|
||||
packageroot = xmlfile.find(PACKAGES_ROOT)
|
||||
packages = xmlroot.findall (PACKAGES_LIST)
|
||||
def prepare_packagefilters():
|
||||
if not packagefilters:
|
||||
return None
|
||||
|
||||
# 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
|
||||
# create simple regexp from given filter
|
||||
for i in range(len(packagefilters)):
|
||||
packagefilters[i] = '^' + packagefilters[i].replace('.', '\.').replace('*', '.*') + '$'
|
||||
|
||||
|
||||
def prepare_packagefilters ():
|
||||
if not packagefilters:
|
||||
return None
|
||||
def include_package(name):
|
||||
if not packagefilters:
|
||||
return True
|
||||
|
||||
# create simple regexp from given filter
|
||||
for i in range (len (packagefilters)):
|
||||
packagefilters[i] = '^' + packagefilters[i].replace ('.', '\.').replace ('*', '.*') + '$'
|
||||
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 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"""
|
||||
|
||||
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)
|
||||
# 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)
|
||||
# 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 = 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
|
||||
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
|
||||
|
|
156
setup.py
156
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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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')],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', '<br>\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 <a href=\"password/\">this form</a>."))
|
||||
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 <a href=\"password/\">this form</a>."
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<role_uuid>[\w+]*)/members/(?P<member_uuid>[^/]+)/$', api_views.role_membership,
|
||||
name='a2-api-role-member'),
|
||||
url(r'^roles/(?P<role_uuid>[\w+]*)/relationships/members/$', api_views.role_memberships,
|
||||
name='a2-api-role-members'),
|
||||
url(
|
||||
r'^roles/(?P<role_uuid>[\w+]*)/members/(?P<member_uuid>[^/]+)/$',
|
||||
api_views.role_membership,
|
||||
name='a2-api-role-member',
|
||||
),
|
||||
url(
|
||||
r'^roles/(?P<role_uuid>[\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'),
|
||||
|
|
|
@ -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'))
|
||||
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 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.
|
||||
"""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.
|
||||
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.
|
||||
"""
|
||||
|
||||
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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;',
|
||||
]
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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:
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'<a href="%s"><img class="%s" src="%s"/></a>' % (
|
||||
value.url, attribute.name, value.url)
|
||||
fragment = u'<a href="%s"><img class="%s" src="%s"/></a>' % (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'),
|
||||
|
|
|
@ -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'))]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,9 +27,9 @@ from ...decorators import to_list
|
|||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
"""
|
||||
Retrieve instances from settings
|
||||
'''
|
||||
"""
|
||||
return [None]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']):
|
||||
|
|
|
@ -21,9 +21,9 @@ from authentic2.backends.ldap_backend import LDAPBackend, LDAPUser
|
|||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
"""
|
||||
Retrieve instances from settings
|
||||
'''
|
||||
"""
|
||||
return [None]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -13,5 +13,5 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel('User'),
|
||||
migrations.DeleteModel('User'),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
"""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')):
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -63,5 +63,3 @@ def check_origin(request, origin):
|
|||
if plugin.check_origin(request, origin):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -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('<h', msg[:2])
|
||||
(msg_length,) = struct.unpack('<h', msg[:2])
|
||||
except struct.error:
|
||||
raise DecryptionError('wrong padding')
|
||||
if len(msg) % block_size != 0:
|
||||
raise DecryptionError('message length is not a multiple of block size', len(msg),
|
||||
block_size)
|
||||
unpadded = msg[2:2 + msg_length]
|
||||
raise DecryptionError('message length is not a multiple of block size', len(msg), block_size)
|
||||
unpadded = msg[2 : 2 + msg_length]
|
||||
if msg_length > 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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)),
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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";',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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";',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 '<User: %r>' % 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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <span class="js-seconds-until">%s</span> seconds '
|
||||
'to try again.')
|
||||
msg = _(
|
||||
'You made too many login errors recently, you must '
|
||||
'wait <span class="js-seconds-until">%s</span> 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
|
||||
|
|
|
@ -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'''
|
||||
<span class="a2-password-check-equality-default">%(default)s</span>
|
||||
<span class="a2-password-check-equality-matched">%(match)s</span>
|
||||
<span class="a2-password-check-equality-unmatched">%(nomatch)s</span>
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'(?<!\w)(' + '|'.join(DATE_FORMAT_PY_JS_MAPPING.keys()) + r')\b')
|
||||
|
@ -131,8 +131,8 @@ class PickerWidgetMixin(object):
|
|||
# with a default, and convert it to a Python data format for later string parsing
|
||||
date_format = self.options['format']
|
||||
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub(
|
||||
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()],
|
||||
date_format)
|
||||
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], date_format
|
||||
)
|
||||
|
||||
super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
|
||||
|
||||
|
@ -147,7 +147,8 @@ class PickerWidgetMixin(object):
|
|||
final_attrs = self.build_attrs(attrs)
|
||||
final_attrs['class'] = "controls input-append date"
|
||||
rendered_widget = super(PickerWidgetMixin, self).render(
|
||||
name, value, attrs=final_attrs, renderer=renderer)
|
||||
name, value, attrs=final_attrs, renderer=renderer
|
||||
)
|
||||
|
||||
# if not set, autoclose have to be true.
|
||||
self.options.setdefault('autoclose', True)
|
||||
|
@ -166,14 +167,18 @@ class PickerWidgetMixin(object):
|
|||
if not help_text:
|
||||
help_text = u'%s %s' % (_('Format:'), self.options['format'])
|
||||
|
||||
return mark_safe(self.render_template % dict(
|
||||
id=id,
|
||||
rendered_widget=rendered_widget,
|
||||
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
|
||||
glyphicon=self.glyphicon,
|
||||
language=get_language(),
|
||||
options=js_options,
|
||||
help_text=help_text))
|
||||
return mark_safe(
|
||||
self.render_template
|
||||
% dict(
|
||||
id=id,
|
||||
rendered_widget=rendered_widget,
|
||||
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
|
||||
glyphicon=self.glyphicon,
|
||||
language=get_language(),
|
||||
options=js_options,
|
||||
help_text=help_text,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DateTimeWidget(PickerWidgetMixin, DateTimeInput):
|
||||
|
@ -255,13 +260,10 @@ class PasswordInput(BasePasswordInput):
|
|||
xstatic('jquery', 'jquery.min.js'),
|
||||
'authentic2/js/password.js',
|
||||
)
|
||||
css = {
|
||||
'all': ('authentic2/css/password.css',)
|
||||
}
|
||||
css = {'all': ('authentic2/css/password.css',)}
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = super(PasswordInput, self).render(
|
||||
name, value, attrs=attrs, renderer=renderer)
|
||||
output = super(PasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
|
||||
if attrs and app_settings.A2_PASSWORD_POLICY_SHOW_LAST_CHAR:
|
||||
_id = attrs.get('id')
|
||||
if _id:
|
||||
|
@ -274,8 +276,7 @@ class NewPasswordInput(PasswordInput):
|
|||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['autocomplete'] = 'new-password'
|
||||
output = super(NewPasswordInput, self).render(
|
||||
name, value, attrs=attrs, renderer=renderer)
|
||||
output = super(NewPasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
|
||||
if attrs:
|
||||
_id = attrs.get('id')
|
||||
if _id:
|
||||
|
@ -290,8 +291,7 @@ class CheckPasswordInput(PasswordInput):
|
|||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['autocomplete'] = 'new-password'
|
||||
output = super(CheckPasswordInput, self).render(
|
||||
name, value, attrs=attrs, renderer=renderer)
|
||||
output = super(CheckPasswordInput, self).render(name, value, attrs=attrs, renderer=renderer)
|
||||
if attrs:
|
||||
_id = attrs.get('id')
|
||||
if _id and _id.endswith('2'):
|
||||
|
@ -326,8 +326,7 @@ class DatalistTextInput(TextInput):
|
|||
self.attrs.update({'list': self.name})
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = super(DatalistTextInput, self).render(
|
||||
name, value, attrs=attrs, renderer=renderer)
|
||||
output = super(DatalistTextInput, self).render(name, value, attrs=attrs, renderer=renderer)
|
||||
datalist = '<datalist id="%s">' % self.name
|
||||
for element in self.data:
|
||||
datalist += '<option value="%s">' % element
|
||||
|
@ -348,14 +347,17 @@ class EmailInput(BaseEmailInput):
|
|||
def media(self):
|
||||
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
|
||||
return forms.Media(
|
||||
js = (
|
||||
js=(
|
||||
xstatic('jquery', 'jquery.min.js'),
|
||||
'authentic2/js/email_domains_suggestions.js',
|
||||
)
|
||||
)
|
||||
|
||||
def get_context(self, *args, **kwargs):
|
||||
context = super(EmailInput, self).get_context(*args, **kwargs)
|
||||
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
|
||||
context['widget']['attrs']['data-suggested-domains'] = ':'.join(app_settings.A2_SUGGESTED_EMAIL_DOMAINS)
|
||||
context['widget']['attrs']['data-suggested-domains'] = ':'.join(
|
||||
app_settings.A2_SUGGESTED_EMAIL_DOMAINS
|
||||
)
|
||||
context['domains_suggested'] = True
|
||||
return context
|
||||
|
|
|
@ -31,6 +31,7 @@ class Drupal7PasswordHasher(hashers.BasePasswordHasher):
|
|||
"""
|
||||
Secure password hashing using the algorithm used by Drupal 7 (recommended)
|
||||
"""
|
||||
|
||||
algorithm = "drupal7_sha512"
|
||||
iterations = 10000
|
||||
digest = hashlib.sha512
|
||||
|
@ -49,20 +50,20 @@ class Drupal7PasswordHasher(hashers.BasePasswordHasher):
|
|||
while i < count:
|
||||
value = v[i]
|
||||
i += 1
|
||||
out += self.i64toa(value & 0x3f)
|
||||
out += self.i64toa(value & 0x3F)
|
||||
if i < count:
|
||||
value |= v[i] << 8
|
||||
out += self.i64toa((value >> 6) & 0x3f)
|
||||
out += self.i64toa((value >> 6) & 0x3F)
|
||||
if i == count:
|
||||
break
|
||||
i += 1
|
||||
if i < count:
|
||||
value |= v[i] << 16
|
||||
out += self.i64toa((value >> 12) & 0x3f)
|
||||
out += self.i64toa((value >> 12) & 0x3F)
|
||||
if i == count:
|
||||
break
|
||||
i += 1
|
||||
out += self.i64toa((value >> 18) & 0x3f)
|
||||
out += self.i64toa((value >> 18) & 0x3F)
|
||||
return out
|
||||
|
||||
def from_drupal(self, encoded):
|
||||
|
@ -95,18 +96,21 @@ class Drupal7PasswordHasher(hashers.BasePasswordHasher):
|
|||
def safe_summary(self, encoded):
|
||||
algorithm, iterations, salt, hash = encoded.split('$', 3)
|
||||
assert algorithm == self.algorithm
|
||||
return OrderedDict([
|
||||
(_('algorithm'), algorithm),
|
||||
(_('iterations'), iterations),
|
||||
(_('salt'), hashers.mask_hash(salt)),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
])
|
||||
return OrderedDict(
|
||||
[
|
||||
(_('algorithm'), algorithm),
|
||||
(_('iterations'), iterations),
|
||||
(_('salt'), hashers.mask_hash(salt)),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class CommonPasswordHasher(hashers.BasePasswordHasher):
|
||||
"""
|
||||
The Salted MD5 password hashing algorithm (not recommended)
|
||||
"""
|
||||
|
||||
algorithm = None
|
||||
digest = None
|
||||
|
||||
|
@ -125,34 +129,20 @@ class CommonPasswordHasher(hashers.BasePasswordHasher):
|
|||
def safe_summary(self, encoded):
|
||||
algorithm, salt, hash = encoded.split('$', 2)
|
||||
assert algorithm == self.algorithm
|
||||
return OrderedDict([
|
||||
(_('algorithm'), algorithm),
|
||||
(_('salt'), hashers.mask_hash(salt, show=2)),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
])
|
||||
return OrderedDict(
|
||||
[
|
||||
(_('algorithm'), algorithm),
|
||||
(_('salt'), hashers.mask_hash(salt, show=2)),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
OPENLDAP_ALGO_MAPPING = {
|
||||
'SHA': (
|
||||
'sha-oldap',
|
||||
0,
|
||||
True
|
||||
),
|
||||
'SSHA': (
|
||||
'ssha-oldap',
|
||||
20,
|
||||
True
|
||||
),
|
||||
'MD5': (
|
||||
'md5-oldap',
|
||||
0,
|
||||
True
|
||||
),
|
||||
'SMD5': (
|
||||
'md5-oldap',
|
||||
16,
|
||||
True
|
||||
),
|
||||
'SHA': ('sha-oldap', 0, True),
|
||||
'SSHA': ('ssha-oldap', 20, True),
|
||||
'MD5': ('md5-oldap', 0, True),
|
||||
'SMD5': ('md5-oldap', 16, True),
|
||||
}
|
||||
|
||||
|
||||
|
@ -301,7 +291,7 @@ class PloneSHA1PasswordHasher(hashers.SHA1PasswordHasher):
|
|||
|
||||
# throw away the prefix
|
||||
if data.startswith(self._prefix):
|
||||
data = data[len(self._prefix):]
|
||||
data = data[len(self._prefix) :]
|
||||
|
||||
# extract salt from encoded data
|
||||
intermediate = base64.b64decode(data)
|
||||
|
@ -313,7 +303,9 @@ class PloneSHA1PasswordHasher(hashers.SHA1PasswordHasher):
|
|||
def safe_summary(self, encoded):
|
||||
algorithm, hash = encoded.split('$', 1)
|
||||
assert algorithm == self.algorithm
|
||||
return OrderedDict([
|
||||
(_('algorithm'), algorithm),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
])
|
||||
return OrderedDict(
|
||||
[
|
||||
(_('algorithm'), algorithm),
|
||||
(_('hash'), hashers.mask_hash(hash)),
|
||||
]
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue