misc: apply black (#52457)

This commit is contained in:
Valentin Deniaud 2021-03-30 10:15:50 +02:00
parent 57ded4fd8f
commit 4bb33d3d3c
339 changed files with 13942 additions and 10703 deletions

View File

@ -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'

View File

@ -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

View File

@ -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())

View File

@ -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.

View File

@ -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
View File

@ -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,
},
)

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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'):

View File

@ -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():

View File

@ -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',

View File

@ -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',),
),
]

View File

@ -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',

View File

@ -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,
),
]

View File

@ -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',
},
),
]

View File

@ -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'),
)
]

View File

@ -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()

View File

@ -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',
},
),
]

View File

@ -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',
},
),
]

View File

@ -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')],
),
),
]

View File

@ -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',),
),
]

View File

@ -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'
),
),
]

View File

@ -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',
),
),
]

View File

@ -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)]

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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'),

View File

@ -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):

View File

@ -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')

View File

@ -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)

View File

@ -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;',
]
],
),
]

View File

@ -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():

View File

@ -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

View File

@ -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,
),
]

View File

@ -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
),
),
]

View File

@ -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'),

View File

@ -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'))]
)

View File

@ -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

View File

@ -27,9 +27,9 @@ from ...decorators import to_list
@to_list
def get_instances(ctx):
'''
"""
Retrieve instances from settings
'''
"""
return [None]

View File

@ -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

View File

@ -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']):

View File

@ -21,9 +21,9 @@ from authentic2.backends.ldap_backend import LDAPBackend, LDAPUser
@to_list
def get_instances(ctx):
'''
"""
Retrieve instances from settings
'''
"""
return [None]

View File

@ -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

View File

@ -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,),
),
]

View File

@ -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',

View File

@ -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,
),
]

View File

@ -13,5 +13,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.DeleteModel('User'),
migrations.DeleteModel('User'),
]

View File

@ -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,

View File

@ -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',
),
),
]

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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')

View File

@ -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]

View File

@ -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()

View File

@ -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

View File

@ -63,5 +63,3 @@ def check_origin(request, origin):
if plugin.check_origin(request, origin):
return True
return False

View File

@ -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):

View File

@ -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

View File

@ -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)),
},
)

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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',

View File

@ -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,
),
]

View File

@ -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',
},
),
]

View File

@ -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,
),
]

View File

@ -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,
),
]

View File

@ -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):

View File

@ -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):

View File

@ -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',
},
),
]

View File

@ -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,
),
]

View File

@ -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',
},
),
]

View File

@ -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],
),
),
]

View File

@ -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',

View File

@ -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";',
),
]

View File

@ -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";',
),
]

View File

@ -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'],
),
]

View File

@ -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:

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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