spring cleaning (#32934)
* reorganize views and forms * add copyright headers to all .py files * fix all style errors reported by flake8
This commit is contained in:
parent
7a0d5719d8
commit
9fbbf0519a
19
MANIFEST.in
19
MANIFEST.in
|
@ -10,20 +10,19 @@ recursive-include src/authentic2/static *.css *.js *.ico *.gif *.png *.jpg
|
|||
recursive-include src/authentic2/manager/static *.css *.js *.png
|
||||
|
||||
# templates
|
||||
recursive-include src/authentic2/saml/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/manager/templates *.html *.txt
|
||||
recursive-include src/authentic2/saml/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/idp/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2_idp_cas/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/idp/saml/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/auth2_auth/auth2_ssl/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/auth2_auth/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/auth2_auth/auth2_oath/templates *.html *.txt *.xml
|
||||
recursive-include src/authentic2/manager/templates *.html
|
||||
recursive-include src/authentic2_auth_saml/templates/authentic2_auth_saml *.html
|
||||
recursive-include src/authentic2_auth_oidc/templates/authentic2_auth_oidc *.html
|
||||
recursive-include src/authentic2_idp_oidc/templates/authentic2_idp_oidc *.html
|
||||
|
||||
recursive-include src/authentic2/vendor/totp_js/js *.js
|
||||
recursive-include src/authentic2/saml/fixtures *.json
|
||||
recursive-include src/authentic2/locale *.po *.mo
|
||||
recursive-include src/authentic2/saml/locale *.po *.mo
|
||||
|
@ -42,26 +41,18 @@ recursive-include src/authentic2_auth_saml/locale *.po *.mo
|
|||
recursive-include src/authentic2_auth_oidc/locale *.po *.mo
|
||||
recursive-include src/authentic2_idp_oidc/locale *.po *.mo
|
||||
|
||||
recursive-include src/authentic2 README xrds.xml *.txt yadis.xrdf
|
||||
recursive-include src/authentic2 README
|
||||
recursive-include src/authentic2_provisionning_ldap/tests *.ldif
|
||||
recursive-include src/authentic2_provisionning_ldap/tests *.ldif
|
||||
|
||||
recursive-include samples *
|
||||
|
||||
include doc/*.rst
|
||||
include doc/pictures/*
|
||||
include COPYING NEWS README.rst AUTHORS.txt
|
||||
include src/authentic2/vendor/oath/TODO
|
||||
include src/authentic2/vendor/totp_js/README.rst
|
||||
include diagnose.py
|
||||
include ez_setup.py
|
||||
include COPYING NEWS README AUTHORS.txt
|
||||
include src/authentic2/auth2_auth/auth2_ssl/authentic_ssl.vhost
|
||||
include requirements.txt
|
||||
include test_settings
|
||||
include getlasso.sh
|
||||
include getlasso3.sh
|
||||
include src/authentic2/nonce/README.rst
|
||||
include doc/conf.py doc/Makefile doc/README.rst.bak
|
||||
include doc/conf.py doc/Makefile
|
||||
include local_settings.py.example
|
||||
include MANIFEST.in
|
||||
include VERSION
|
||||
|
|
485
ez_setup.py
485
ez_setup.py
|
@ -1,485 +0,0 @@
|
|||
#!python
|
||||
"""Bootstrap distribute installation
|
||||
|
||||
If you want to use setuptools in your package's setup.py, just include this
|
||||
file in the same directory with it, and add this to the top of your setup.py::
|
||||
|
||||
from distribute_setup import use_setuptools
|
||||
use_setuptools()
|
||||
|
||||
If you want to require a specific version of setuptools, set a download
|
||||
mirror, or use an alternate download directory, you can do so by supplying
|
||||
the appropriate options to ``use_setuptools()``.
|
||||
|
||||
This file can also be run as a script to install or upgrade setuptools.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import fnmatch
|
||||
import tempfile
|
||||
import tarfile
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from site import USER_SITE
|
||||
except ImportError:
|
||||
USER_SITE = None
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
return subprocess.call(args) == 0
|
||||
|
||||
except ImportError:
|
||||
# will be used for python 2.3
|
||||
def _python_cmd(*args):
|
||||
args = (sys.executable,) + args
|
||||
# quoting arguments if windows
|
||||
if sys.platform == 'win32':
|
||||
def quote(arg):
|
||||
if ' ' in arg:
|
||||
return '"%s"' % arg
|
||||
return arg
|
||||
args = [quote(arg) for arg in args]
|
||||
return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
|
||||
|
||||
DEFAULT_VERSION = "0.6.14"
|
||||
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
|
||||
SETUPTOOLS_FAKED_VERSION = "0.6c11"
|
||||
|
||||
SETUPTOOLS_PKG_INFO = """\
|
||||
Metadata-Version: 1.0
|
||||
Name: setuptools
|
||||
Version: %s
|
||||
Summary: xxxx
|
||||
Home-page: xxx
|
||||
Author: xxx
|
||||
Author-email: xxx
|
||||
License: xxx
|
||||
Description: xxx
|
||||
""" % SETUPTOOLS_FAKED_VERSION
|
||||
|
||||
|
||||
def _install(tarball):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# installing
|
||||
log.warn('Installing Distribute')
|
||||
if not _python_cmd('setup.py', 'install'):
|
||||
log.warn('Something went wrong during the installation.')
|
||||
log.warn('See the error message above.')
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
|
||||
|
||||
def _build_egg(egg, tarball, to_dir):
|
||||
# extracting the tarball
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
tar = tarfile.open(tarball)
|
||||
_extractall(tar)
|
||||
tar.close()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
|
||||
# building an egg
|
||||
log.warn('Building a Distribute egg in %s', to_dir)
|
||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
# returning the result
|
||||
log.warn(egg)
|
||||
if not os.path.exists(egg):
|
||||
raise IOError('Could not build the egg.')
|
||||
|
||||
|
||||
def _do_download(version, download_base, to_dir, download_delay):
|
||||
egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
|
||||
% (version, sys.version_info[0], sys.version_info[1]))
|
||||
if not os.path.exists(egg):
|
||||
tarball = download_setuptools(version, download_base,
|
||||
to_dir, download_delay)
|
||||
_build_egg(egg, tarball, to_dir)
|
||||
sys.path.insert(0, egg)
|
||||
import setuptools
|
||||
setuptools.bootstrap_install_from = egg
|
||||
|
||||
|
||||
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, download_delay=15, no_fake=True):
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
was_imported = 'pkg_resources' in sys.modules or \
|
||||
'setuptools' in sys.modules
|
||||
try:
|
||||
try:
|
||||
import pkg_resources
|
||||
if not hasattr(pkg_resources, '_distribute'):
|
||||
if not no_fake:
|
||||
_fake_setuptools()
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
try:
|
||||
pkg_resources.require("distribute>="+version)
|
||||
return
|
||||
except pkg_resources.VersionConflict:
|
||||
e = sys.exc_info()[1]
|
||||
if was_imported:
|
||||
sys.stderr.write(
|
||||
"The required version of distribute (>=%s) is not available,\n"
|
||||
"and can't be installed while this script is running. Please\n"
|
||||
"install a more recent version first, using\n"
|
||||
"'easy_install -U distribute'."
|
||||
"\n\n(Currently using %r)\n" % (version, e.args[0]))
|
||||
sys.exit(2)
|
||||
else:
|
||||
del pkg_resources, sys.modules['pkg_resources'] # reload ok
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return _do_download(version, download_base, to_dir,
|
||||
download_delay)
|
||||
finally:
|
||||
if not no_fake:
|
||||
_create_fake_setuptools_pkg_info(to_dir)
|
||||
|
||||
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, delay=15):
|
||||
"""Download distribute from a specified location and return its filename
|
||||
|
||||
`version` should be a valid distribute version number that is available
|
||||
as an egg for download under the `download_base` URL (which should end
|
||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||
`delay` is the number of seconds to pause before an actual download
|
||||
attempt.
|
||||
"""
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
tgz_name = "distribute-%s.tar.gz" % version
|
||||
url = download_base + tgz_name
|
||||
saveto = os.path.join(to_dir, tgz_name)
|
||||
src = dst = None
|
||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||
try:
|
||||
log.warn("Downloading %s", url)
|
||||
src = urlopen(url)
|
||||
# Read/write all in one block, so we don't create a corrupt file
|
||||
# if the download is interrupted.
|
||||
data = src.read()
|
||||
dst = open(saveto, "wb")
|
||||
dst.write(data)
|
||||
finally:
|
||||
if src:
|
||||
src.close()
|
||||
if dst:
|
||||
dst.close()
|
||||
return os.path.realpath(saveto)
|
||||
|
||||
def _no_sandbox(function):
|
||||
def __no_sandbox(*args, **kw):
|
||||
try:
|
||||
from setuptools.sandbox import DirectorySandbox
|
||||
if not hasattr(DirectorySandbox, '_old'):
|
||||
def violation(*args):
|
||||
pass
|
||||
DirectorySandbox._old = DirectorySandbox._violation
|
||||
DirectorySandbox._violation = violation
|
||||
patched = True
|
||||
else:
|
||||
patched = False
|
||||
except ImportError:
|
||||
patched = False
|
||||
|
||||
try:
|
||||
return function(*args, **kw)
|
||||
finally:
|
||||
if patched:
|
||||
DirectorySandbox._violation = DirectorySandbox._old
|
||||
del DirectorySandbox._old
|
||||
|
||||
return __no_sandbox
|
||||
|
||||
def _patch_file(path, content):
|
||||
"""Will backup the file then patch it"""
|
||||
existing_content = open(path).read()
|
||||
if existing_content == content:
|
||||
# already patched
|
||||
log.warn('Already patched.')
|
||||
return False
|
||||
log.warn('Patching...')
|
||||
_rename_path(path)
|
||||
f = open(path, 'w')
|
||||
try:
|
||||
f.write(content)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_file = _no_sandbox(_patch_file)
|
||||
|
||||
def _same_content(path, content):
|
||||
return open(path).read() == content
|
||||
|
||||
def _rename_path(path):
|
||||
new_name = path + '.OLD.%s' % time.time()
|
||||
log.warn('Renaming %s into %s', path, new_name)
|
||||
os.rename(path, new_name)
|
||||
return new_name
|
||||
|
||||
def _remove_flat_installation(placeholder):
|
||||
if not os.path.isdir(placeholder):
|
||||
log.warn('Unkown installation at %s', placeholder)
|
||||
return False
|
||||
found = False
|
||||
for file in os.listdir(placeholder):
|
||||
if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
log.warn('Could not locate setuptools*.egg-info')
|
||||
return
|
||||
|
||||
log.warn('Removing elements out of the way...')
|
||||
pkg_info = os.path.join(placeholder, file)
|
||||
if os.path.isdir(pkg_info):
|
||||
patched = _patch_egg_dir(pkg_info)
|
||||
else:
|
||||
patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
|
||||
|
||||
if not patched:
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
# now let's move the files out of the way
|
||||
for element in ('setuptools', 'pkg_resources.py', 'site.py'):
|
||||
element = os.path.join(placeholder, element)
|
||||
if os.path.exists(element):
|
||||
_rename_path(element)
|
||||
else:
|
||||
log.warn('Could not find the %s element of the '
|
||||
'Setuptools distribution', element)
|
||||
return True
|
||||
|
||||
_remove_flat_installation = _no_sandbox(_remove_flat_installation)
|
||||
|
||||
def _after_install(dist):
|
||||
log.warn('After install bootstrap.')
|
||||
placeholder = dist.get_command_obj('install').install_purelib
|
||||
_create_fake_setuptools_pkg_info(placeholder)
|
||||
|
||||
def _create_fake_setuptools_pkg_info(placeholder):
|
||||
if not placeholder or not os.path.exists(placeholder):
|
||||
log.warn('Could not find the install location')
|
||||
return
|
||||
pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
|
||||
setuptools_file = 'setuptools-%s-py%s.egg-info' % \
|
||||
(SETUPTOOLS_FAKED_VERSION, pyver)
|
||||
pkg_info = os.path.join(placeholder, setuptools_file)
|
||||
if os.path.exists(pkg_info):
|
||||
log.warn('%s already exists', pkg_info)
|
||||
return
|
||||
|
||||
log.warn('Creating %s', pkg_info)
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
pth_file = os.path.join(placeholder, 'setuptools.pth')
|
||||
log.warn('Creating %s', pth_file)
|
||||
f = open(pth_file, 'w')
|
||||
try:
|
||||
f.write(os.path.join(os.curdir, setuptools_file))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
|
||||
|
||||
def _patch_egg_dir(path):
|
||||
# let's check if it's already patched
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
if os.path.exists(pkg_info):
|
||||
if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
|
||||
log.warn('%s already patched.', pkg_info)
|
||||
return False
|
||||
_rename_path(path)
|
||||
os.mkdir(path)
|
||||
os.mkdir(os.path.join(path, 'EGG-INFO'))
|
||||
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
|
||||
f = open(pkg_info, 'w')
|
||||
try:
|
||||
f.write(SETUPTOOLS_PKG_INFO)
|
||||
finally:
|
||||
f.close()
|
||||
return True
|
||||
|
||||
_patch_egg_dir = _no_sandbox(_patch_egg_dir)
|
||||
|
||||
def _before_install():
|
||||
log.warn('Before install bootstrap.')
|
||||
_fake_setuptools()
|
||||
|
||||
|
||||
def _under_prefix(location):
|
||||
if 'install' not in sys.argv:
|
||||
return True
|
||||
args = sys.argv[sys.argv.index('install')+1:]
|
||||
for index, arg in enumerate(args):
|
||||
for option in ('--root', '--prefix'):
|
||||
if arg.startswith('%s=' % option):
|
||||
top_dir = arg.split('root=')[-1]
|
||||
return location.startswith(top_dir)
|
||||
elif arg == option:
|
||||
if len(args) > index:
|
||||
top_dir = args[index+1]
|
||||
return location.startswith(top_dir)
|
||||
if arg == '--user' and USER_SITE is not None:
|
||||
return location.startswith(USER_SITE)
|
||||
return True
|
||||
|
||||
|
||||
def _fake_setuptools():
|
||||
log.warn('Scanning installed packages')
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
# we're cool
|
||||
log.warn('Setuptools or Distribute does not seem to be installed.')
|
||||
return
|
||||
ws = pkg_resources.working_set
|
||||
try:
|
||||
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
|
||||
replacement=False))
|
||||
except TypeError:
|
||||
# old distribute API
|
||||
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
|
||||
|
||||
if setuptools_dist is None:
|
||||
log.warn('No setuptools distribution found')
|
||||
return
|
||||
# detecting if it was already faked
|
||||
setuptools_location = setuptools_dist.location
|
||||
log.warn('Setuptools installation detected at %s', setuptools_location)
|
||||
|
||||
# if --root or --preix was provided, and if
|
||||
# setuptools is not located in them, we don't patch it
|
||||
if not _under_prefix(setuptools_location):
|
||||
log.warn('Not patching, --root or --prefix is installing Distribute'
|
||||
' in another location')
|
||||
return
|
||||
|
||||
# let's see if its an egg
|
||||
if not setuptools_location.endswith('.egg'):
|
||||
log.warn('Non-egg installation')
|
||||
res = _remove_flat_installation(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
else:
|
||||
log.warn('Egg installation')
|
||||
pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
|
||||
if (os.path.exists(pkg_info) and
|
||||
_same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
|
||||
log.warn('Already patched.')
|
||||
return
|
||||
log.warn('Patching...')
|
||||
# let's create a fake egg replacing setuptools one
|
||||
res = _patch_egg_dir(setuptools_location)
|
||||
if not res:
|
||||
return
|
||||
log.warn('Patched done.')
|
||||
_relaunch()
|
||||
|
||||
|
||||
def _relaunch():
|
||||
log.warn('Relaunching...')
|
||||
# we have to relaunch the process
|
||||
# pip marker to avoid a relaunch bug
|
||||
if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
|
||||
sys.argv[0] = 'setup.py'
|
||||
args = [sys.executable] + sys.argv
|
||||
sys.exit(subprocess.call(args))
|
||||
|
||||
|
||||
def _extractall(self, path=".", members=None):
|
||||
"""Extract all members from the archive to the current working
|
||||
directory and set owner, modification time and permissions on
|
||||
directories afterwards. `path' specifies a different directory
|
||||
to extract to. `members' is optional and must be a subset of the
|
||||
list returned by getmembers().
|
||||
"""
|
||||
import copy
|
||||
import operator
|
||||
from tarfile import ExtractError
|
||||
directories = []
|
||||
|
||||
if members is None:
|
||||
members = self
|
||||
|
||||
for tarinfo in members:
|
||||
if tarinfo.isdir():
|
||||
# Extract directories with a safe mode.
|
||||
directories.append(tarinfo)
|
||||
tarinfo = copy.copy(tarinfo)
|
||||
tarinfo.mode = 448 # decimal for oct 0700
|
||||
self.extract(tarinfo, path)
|
||||
|
||||
# Reverse sort directories.
|
||||
if sys.version_info < (2, 4):
|
||||
def sorter(dir1, dir2):
|
||||
return cmp(dir1.name, dir2.name)
|
||||
directories.sort(sorter)
|
||||
directories.reverse()
|
||||
else:
|
||||
directories.sort(key=operator.attrgetter('name'), reverse=True)
|
||||
|
||||
# Set correct owner, mtime and filemode on directories.
|
||||
for tarinfo in directories:
|
||||
dirpath = os.path.join(path, tarinfo.name)
|
||||
try:
|
||||
self.chown(tarinfo, dirpath)
|
||||
self.utime(tarinfo, dirpath)
|
||||
self.chmod(tarinfo, dirpath)
|
||||
except ExtractError:
|
||||
e = sys.exc_info()[1]
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
|
||||
|
||||
def main(argv, version=DEFAULT_VERSION):
|
||||
"""Install or upgrade setuptools and EasyInstall"""
|
||||
tarball = download_setuptools()
|
||||
_install(tarball)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
|
@ -1,2 +0,0 @@
|
|||
authentic2-plugin-template is entirely under the copyright of Entr'ouvert and
|
||||
distributed under the license AGPLv3 or later.
|
|
@ -1,3 +0,0 @@
|
|||
include COPYING
|
||||
recursive-include src/authentic2_plugin_template/templates *.html
|
||||
recursive-include src/authentic2_plugin_template/static *.js *.css *.png
|
|
@ -1,20 +0,0 @@
|
|||
** THIS IS A TEMPLATE PROJECT **
|
||||
|
||||
To rename it to your taste:
|
||||
|
||||
$ ./adapt.sh
|
||||
|
||||
** THIS IS A TEMPLATE PROJECT **
|
||||
Authentic2 Plugin Template
|
||||
==========================
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
You just have to install the package in your virtualenv and relaunch, it will
|
||||
be automatically loaded by authentic2.
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
** DESCRIBE CUSTOM SETTINGS HERE **
|
|
@ -1,38 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -x
|
||||
|
||||
echo "Give project name (it must match regexp ^[a-z][a-z0-9-]+$ )"
|
||||
read PROJECT_NAME
|
||||
|
||||
if ! echo $PROJECT_NAME | grep -q '^[a-z][a-z0-9-]\+$'; then
|
||||
echo "Invalid project name:" $PROJECT_NAME
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UPPER_UNDERSCORED=`echo $PROJECT_NAME | tr a-z A-Z | sed 's/-/_/g'`
|
||||
LOWER_UNDERSCORED=`echo $PROJECT_NAME | sed 's/-/_/g'`
|
||||
TITLECASE=`echo $PROJECT_NAME | sed 's/-/ /g;s/.*/\L&/; s/[a-z]*/\u&/g'`
|
||||
|
||||
echo Project name: $PROJECT_NAME
|
||||
echo Uppercase underscored: $UPPER_UNDERSCORED
|
||||
echo Lowercase underscored: $LOWER_UNDERSCORED
|
||||
echo Titlecase: $TITLECASE
|
||||
|
||||
if [ -d .git ]; then
|
||||
MV='git mv'
|
||||
else
|
||||
MV=mv
|
||||
fi
|
||||
|
||||
sed -i \
|
||||
-e "s/authentic2_plugin_template/$LOWER_UNDERSCORED/g" \
|
||||
-e "s/authentic2-plugin-template/$PROJECT_NAME/g" \
|
||||
-e "s/A2_TEMPLATE_/A2_$UPPER_UNDERSCORED_/g" \
|
||||
-e "s/Authentic2 Plugin Template/$TITLECASE/g" \
|
||||
setup.py src/*/*.py README COPYING MANIFEST.in
|
||||
$MV src/authentic2_plugin_template/static/authentic2_plugin_template \
|
||||
src/authentic2_plugin_template/static/$LOWER_UNDERSCORED
|
||||
$MV src/authentic2_plugin_template/templates/authentic2_plugin_template \
|
||||
src/authentic2_plugin_template/templates/$LOWER_UNDERSCORED
|
||||
$MV src/authentic2_plugin_template src/$LOWER_UNDERSCORED
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
import subprocess
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
|
||||
def get_version():
|
||||
'''Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.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','--match=v*'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
return result.split()[0][1:].replace('-', '.')
|
||||
else:
|
||||
return '0.0.0-%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.0'
|
||||
|
||||
README = file(os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'README')).read()
|
||||
|
||||
setup(name='authentic2-plugin-template',
|
||||
version=get_version(),
|
||||
license='AGPLv3',
|
||||
description='Authentic2 Plugin Template',
|
||||
long_description=README,
|
||||
author="Entr'ouvert",
|
||||
author_email="info@entrouvert.com",
|
||||
packages=find_packages('src'),
|
||||
package_dir={
|
||||
'': 'src',
|
||||
},
|
||||
package_data={
|
||||
'authentic2_plugin_template': [
|
||||
'templates/authentic2_plugin_template/*.html',
|
||||
'static/authentic2_plugin_template/js/*.js',
|
||||
'static/authentic2_plugin_template/css/*.css',
|
||||
'static/authentic2_plugin_template/img/*.png',
|
||||
],
|
||||
},
|
||||
install_requires=[
|
||||
],
|
||||
entry_points={
|
||||
'authentic2.plugin': [
|
||||
'authentic2-plugin-template= authentic2_plugin_template:Plugin',
|
||||
],
|
||||
},
|
||||
)
|
|
@ -1,58 +0,0 @@
|
|||
__version__ = '1.0.0'
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_after_urls(self):
|
||||
return []
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def get_before_middleware(self):
|
||||
return []
|
||||
|
||||
def get_after_middleware(self):
|
||||
return []
|
||||
|
||||
def get_authentication_backends(self):
|
||||
return []
|
||||
|
||||
def get_auth_frontends(self):
|
||||
return []
|
||||
|
||||
def get_idp_backends(self):
|
||||
return []
|
||||
|
||||
def service_list(self, request):
|
||||
'''For IdP plugins this method add links to the user homepage.
|
||||
|
||||
It must return a list of authentic2.utils.Service objects, each
|
||||
object has a name and can have an url and some actions.
|
||||
|
||||
Service(name=name[, url=url[, actions=actions]])
|
||||
|
||||
Actions are a list of tuples, whose parts are
|
||||
- first the name of the action,
|
||||
- the HTTP method for calling the action,
|
||||
- the URL for calling the action,
|
||||
- the paramters to pass to this URL as a sequence of key-value tuples.
|
||||
'''
|
||||
return []
|
||||
|
||||
def logout_list(self, request):
|
||||
'''For IdP or SP plugins this method add actions to logout from remote
|
||||
IdP or SP.
|
||||
|
||||
It must returns a list of HTML fragments, each fragment is
|
||||
responsible for calling the view doing the logout. Views are usually
|
||||
called using <img/> or <iframge/> tags and finally redirect to an
|
||||
icon indicating success or failure for the logout.
|
||||
|
||||
Authentic2 provide two such icons through the following URLs:
|
||||
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png')
|
||||
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png')
|
||||
'''
|
||||
return []
|
|
@ -1,5 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
# registrer your admin editable models here using admin.register
|
|
@ -1,23 +0,0 @@
|
|||
class AppSettings(object):
|
||||
__DEFAULTS = {
|
||||
'ENABLE': True,
|
||||
}
|
||||
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
|
||||
def _setting(self, name, dflt):
|
||||
from django.conf import settings
|
||||
return getattr(settings, self.prefix+name, dflt)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self.__DEFAULTS:
|
||||
raise AttributeError(name)
|
||||
return self._setting(name, self.__DEFAULTS[name])
|
||||
|
||||
# Ugly? Guido recommends this himself ...
|
||||
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||
import sys
|
||||
app_settings = AppSettings('A2_PLUGIN_TEMPLATE_')
|
||||
app_settings.__name__ = __name__
|
||||
sys.modules[__name__] = app_settings
|
|
@ -1,3 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# put your models here
|
|
@ -1 +0,0 @@
|
|||
{% comment %}placeholder{% endcomment %}
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
from . import app_settings
|
||||
from .views import index
|
||||
|
||||
urlpatterns = required(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[url('^authentic2_plugin_template/$', index, name='authentic2-plugin-template-index')]
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
|
||||
from . import decorators
|
||||
|
||||
__ALL_ = [ 'sso' ]
|
||||
|
||||
@decorators.plugin_enabled
|
||||
def index(request):
|
||||
return render(request, 'authentic2_plugin_template/index.html')
|
|
@ -1 +1,17 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
default_app_config = 'authentic2.apps.Authentic2Config'
|
||||
|
|
|
@ -1 +1,17 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
default_app_config = 'authentic2.a2_rbac.apps.Authentic2RBACConfig'
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import six
|
||||
|
|
|
@ -1,3 +1,22 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class AppSettings(object):
|
||||
__DEFAULTS = dict(
|
||||
MANAGED_CONTENT_TYPES=None,
|
||||
|
@ -21,7 +40,6 @@ class AppSettings(object):
|
|||
|
||||
# Ugly? Guido recommends this himself ...
|
||||
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||
import sys
|
||||
app_settings = AppSettings('A2_RBAC_')
|
||||
app_settings.__name__ = __name__
|
||||
sys.modules[__name__] = app_settings
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
@ -7,9 +23,7 @@ class Authentic2RBACConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
from . import signal_handlers, models
|
||||
from django.db.models.signals import post_save, post_migrate, pre_save, \
|
||||
post_delete
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.signals import post_save, post_migrate, post_delete
|
||||
from authentic2.models import Service
|
||||
|
||||
# update rbac on save to contenttype, ou and roles
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db.models import NullBooleanField
|
||||
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
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.text import slugify
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.apps import apps
|
||||
|
||||
from django_rbac.utils import get_role_model, get_ou_model, \
|
||||
get_permission_model
|
||||
|
||||
from ..utils import get_fk_model
|
||||
from . import utils, app_settings, signal_handlers
|
||||
from . import utils, app_settings
|
||||
|
||||
|
||||
def update_ou_admin_roles(ou):
|
||||
|
@ -59,8 +73,6 @@ def update_ous_admin_roles():
|
|||
they give general administrative rights to all mamanged content types
|
||||
scoped to the given organizational unit.
|
||||
'''
|
||||
Role = get_role_model()
|
||||
Permission = get_permission_model()
|
||||
OU = get_ou_model()
|
||||
ou_all = OU.objects.all()
|
||||
if len(ou_all) < 2:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from django_rbac.models import ADMIN_OP
|
||||
|
@ -60,7 +76,8 @@ class RoleManager(BaseRoleManager):
|
|||
defaults={
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
}, **kwargs)
|
||||
},
|
||||
**kwargs)
|
||||
if update_name and not created and role.name != name:
|
||||
role.name = name
|
||||
role.save()
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import namedtuple
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import six
|
||||
|
@ -5,7 +21,6 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.utils.text import slugify
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
from django_rbac.models import (RoleAbstractBase, PermissionAbstractBase,
|
||||
OrganizationalUnitAbstractBase, RoleParentingAbstractBase, VIEW_OP,
|
||||
|
@ -32,17 +47,17 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase):
|
|||
MANUAL_PASSWORD_POLICY = 1
|
||||
|
||||
USER_ADD_PASSWD_POLICY_CHOICES = (
|
||||
(RESET_LINK_POLICY, _('Send reset link')),
|
||||
(MANUAL_PASSWORD_POLICY, _('Manual password definition')),
|
||||
(RESET_LINK_POLICY, _('Send reset link')),
|
||||
(MANUAL_PASSWORD_POLICY, _('Manual password definition')),
|
||||
)
|
||||
|
||||
PolicyValue = namedtuple('PolicyValue', [
|
||||
'generate_password', 'reset_password_at_next_login',
|
||||
'send_mail', 'send_password_reset'])
|
||||
'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),
|
||||
RESET_LINK_POLICY: PolicyValue(False, False, False, True),
|
||||
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
|
||||
}
|
||||
|
||||
username_is_unique = models.BooleanField(
|
||||
|
@ -247,8 +262,11 @@ class Role(RoleAbstractBase):
|
|||
)
|
||||
|
||||
def natural_key(self):
|
||||
return [self.slug, self.ou and self.ou.natural_key(), self.service and
|
||||
self.service.natural_key()]
|
||||
return [
|
||||
self.slug,
|
||||
self.ou and self.ou.natural_key(),
|
||||
self.service and self.service.natural_key(),
|
||||
]
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
|
@ -8,6 +24,7 @@ from ..utils import get_fk_model
|
|||
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):
|
||||
if not router.allow_migrate(using, get_ou_model()):
|
||||
|
@ -36,9 +53,7 @@ def create_default_ou(app_config, verbosity=2, interactive=True,
|
|||
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_ou_admin_roles, update_ous_admin_roles, \
|
||||
update_content_types_roles
|
||||
|
||||
from .management import update_ous_admin_roles, update_content_types_roles
|
||||
|
||||
if not router.allow_migrate(using, get_role_model()):
|
||||
return
|
||||
|
@ -50,16 +65,17 @@ def post_migrate_update_rbac(app_config, verbosity=2, interactive=True,
|
|||
|
||||
|
||||
def update_rbac_on_ou_post_save(sender, instance, created, raw, **kwargs):
|
||||
from .management import update_ou_admin_roles, update_ous_admin_roles, \
|
||||
update_content_types_roles
|
||||
from .management import update_ou_admin_roles, update_ous_admin_roles
|
||||
|
||||
if get_ou_model().objects.count() < 3 and created:
|
||||
update_ous_admin_roles()
|
||||
else:
|
||||
update_ou_admin_roles(instance)
|
||||
|
||||
|
||||
def update_rbac_on_ou_post_delete(sender, instance, **kwargs):
|
||||
from .management import update_ou_admin_roles, update_ous_admin_roles, \
|
||||
update_content_types_roles
|
||||
from .management import update_ous_admin_roles
|
||||
|
||||
if get_ou_model().objects.count() < 2:
|
||||
update_ous_admin_roles()
|
||||
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth import get_user_model
|
||||
|
@ -10,7 +26,6 @@ Role = get_role_model()
|
|||
User = get_user_model()
|
||||
|
||||
|
||||
|
||||
class A2RBACTestCase(TestCase):
|
||||
def test_update_rbac(self):
|
||||
# 3 content types managers and 1 global manager
|
||||
|
@ -73,7 +88,6 @@ class A2RBACTestCase(TestCase):
|
|||
self.assertTrue(role.slug.startswith('_a2'), u'role %s slug must '
|
||||
'start with _a2: %s' % (role.name, role.slug))
|
||||
|
||||
|
||||
def test_admin_roles_update_slug(self):
|
||||
user = User.objects.create(username='john.doe')
|
||||
name1 = 'Can manage john.doe'
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_rbac.models import VIEW_OP, SEARCH_OP
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from copy import deepcopy
|
||||
import pprint
|
||||
|
||||
|
@ -5,26 +21,25 @@ from django.contrib import admin
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.admin.utils import flatten_fieldsets
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
|
||||
from .nonce.models import Nonce
|
||||
from . import (models, compat, app_settings, decorators,
|
||||
attribute_kinds, utils)
|
||||
from .forms import modelform_factory, BaseUserForm
|
||||
from . import (models, app_settings, decorators, attribute_kinds,
|
||||
utils)
|
||||
from .forms.profile import BaseUserForm, modelform_factory
|
||||
from .custom_user.models import User
|
||||
|
||||
|
||||
def cleanup_action(modeladmin, request, queryset):
|
||||
queryset.cleanup()
|
||||
cleanup_action.short_description = _('Cleanup expired objects')
|
||||
|
||||
|
||||
class CleanupAdminMixin(admin.ModelAdmin):
|
||||
def get_actions(self, request):
|
||||
actions = super(CleanupAdminMixin, self).get_actions(request)
|
||||
|
@ -32,16 +47,25 @@ class CleanupAdminMixin(admin.ModelAdmin):
|
|||
actions['cleanup_action'] = cleanup_action, 'cleanup_action', cleanup_action.short_description
|
||||
return actions
|
||||
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
|
||||
class AuthenticationEventAdmin(admin.ModelAdmin):
|
||||
list_display = ('when', 'who', 'how', 'nonce')
|
||||
list_filter = ('how',)
|
||||
|
@ -49,12 +73,17 @@ class AuthenticationEventAdmin(admin.ModelAdmin):
|
|||
search_fields = ('who', 'nonce', 'how')
|
||||
|
||||
admin.site.register(models.AuthenticationEvent, AuthenticationEventAdmin)
|
||||
|
||||
|
||||
class UserExternalIdAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'source', 'external_id', 'created', 'updated')
|
||||
list_filter = ('source',)
|
||||
date_hierarchy = 'created'
|
||||
search_fields = ('user__username', 'source', 'external_id')
|
||||
|
||||
admin.site.register(models.UserExternalId, UserExternalIdAdmin)
|
||||
|
||||
|
||||
class DeletedUserAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'creation')
|
||||
date_hierarchy = 'creation'
|
||||
|
@ -96,7 +125,7 @@ if settings.SESSION_ENGINE in DB_SESSION_ENGINES:
|
|||
backend = auth.load_backend(backend_class)
|
||||
try:
|
||||
user = backend.get_user(user_id) or auth_models.AnonymousUser()
|
||||
except:
|
||||
except Exception:
|
||||
user = _('deleted user %r') % user_id
|
||||
return user
|
||||
user.short_description = _('user')
|
||||
|
@ -107,6 +136,7 @@ if settings.SESSION_ENGINE in DB_SESSION_ENGINES:
|
|||
|
||||
admin.site.register(Session, SessionAdmin)
|
||||
|
||||
|
||||
class ExternalUserListFilter(admin.SimpleListFilter):
|
||||
title = _('external')
|
||||
|
||||
|
@ -114,8 +144,8 @@ class ExternalUserListFilter(admin.SimpleListFilter):
|
|||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('1', _('Yes')),
|
||||
('0', _('No'))
|
||||
('1', _('Yes')),
|
||||
('0', _('No'))
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
|
@ -130,6 +160,7 @@ class ExternalUserListFilter(admin.SimpleListFilter):
|
|||
return queryset.filter(userexternalid__isnull=True)
|
||||
return queryset
|
||||
|
||||
|
||||
class UserRealmListFilter(admin.SimpleListFilter):
|
||||
# Human-readable title which will be displayed in the
|
||||
# right admin sidebar just above the filter options.
|
||||
|
@ -164,7 +195,8 @@ class UserChangeForm(BaseUserForm):
|
|||
'missing_credential': _("You must at least give a username or an email to your user"),
|
||||
}
|
||||
|
||||
password = ReadOnlyPasswordHashField(label=_("Password"),
|
||||
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>."))
|
||||
|
@ -192,6 +224,7 @@ class UserChangeForm(BaseUserForm):
|
|||
code='missing_credential',
|
||||
)
|
||||
|
||||
|
||||
class UserCreationForm(BaseUserForm):
|
||||
"""
|
||||
A form that creates a user, with no privileges, from the given username and
|
||||
|
@ -201,9 +234,11 @@ class UserCreationForm(BaseUserForm):
|
|||
'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"),
|
||||
password1 = forms.CharField(
|
||||
label=_("Password"),
|
||||
widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(label=_("Password confirmation"),
|
||||
password2 = forms.CharField(
|
||||
label=_("Password confirmation"),
|
||||
widget=forms.PasswordInput,
|
||||
help_text=_("Enter the same password as above, for verification."))
|
||||
|
||||
|
@ -235,6 +270,7 @@ class UserCreationForm(BaseUserForm):
|
|||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class AuthenticUserAdmin(UserAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('uuid', 'ou', 'password')}),
|
||||
|
@ -244,21 +280,19 @@ class AuthenticUserAdmin(UserAdmin):
|
|||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
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)
|
||||
list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter)
|
||||
list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email']
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj))
|
||||
if obj:
|
||||
if not request.user.is_superuser:
|
||||
fieldsets[2][1]['fields'] = filter(lambda x: x !=
|
||||
'is_superuser', fieldsets[2][1]['fields'])
|
||||
fieldsets[2][1]['fields'] = filter(lambda x: x != 'is_superuser', fieldsets[2][1]['fields'])
|
||||
qs = models.Attribute.objects.all()
|
||||
insertion_idx = 2
|
||||
else:
|
||||
|
@ -292,6 +326,8 @@ class AuthenticUserAdmin(UserAdmin):
|
|||
kwargs['fields'] = fields
|
||||
return super(AuthenticUserAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
|
||||
admin.site.register(User, AuthenticUserAdmin)
|
||||
|
||||
|
||||
class AttributeForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -318,7 +354,6 @@ class AttributeAdmin(admin.ModelAdmin):
|
|||
def get_queryset(self, request):
|
||||
return self.model.all_objects.all()
|
||||
|
||||
|
||||
admin.site.register(models.Attribute, AttributeAdmin)
|
||||
|
||||
|
||||
|
@ -328,6 +363,7 @@ def login(request, extra_context=None):
|
|||
|
||||
admin.site.login = login
|
||||
|
||||
|
||||
@never_cache
|
||||
def logout(request, extra_context=None):
|
||||
return utils.redirect_to_login(request, login_url='auth_logout')
|
||||
|
@ -335,4 +371,3 @@ def logout(request, extra_context=None):
|
|||
admin.site.logout = logout
|
||||
|
||||
admin.site.register(models.PasswordReset)
|
||||
admin.site.register(User, AuthenticUserAdmin)
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import api_views
|
||||
|
||||
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_memberships, name='a2-api-role-member'),
|
||||
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'^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_memberships,
|
||||
name='a2-api-role-member'),
|
||||
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'),
|
||||
]
|
||||
|
||||
urlpatterns += api_views.router.urls
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2018 Entr'ouvert
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
|
@ -20,7 +20,6 @@ import smtplib
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.encoding import force_text
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
@ -138,8 +137,8 @@ class RegistrationSerializer(serializers.Serializer):
|
|||
User.objects.filter(ou=ou, email__iexact=data['email']).exists():
|
||||
raise serializers.ValidationError(
|
||||
_('You already have an account'))
|
||||
if (ou.username_is_unique and
|
||||
'username' not in data):
|
||||
if (ou.username_is_unique
|
||||
and 'username' not in data):
|
||||
raise serializers.ValidationError(
|
||||
_('Username is required in this ou'))
|
||||
if ou.username_is_unique and User.objects.filter(
|
||||
|
@ -779,7 +778,6 @@ class CheckPasswordAPI(BaseRpcView):
|
|||
result['errors'] = [exc.detail]
|
||||
return result, status.HTTP_200_OK
|
||||
|
||||
|
||||
check_password = CheckPasswordAPI.as_view()
|
||||
|
||||
|
||||
|
@ -811,5 +809,4 @@ class ValidatePasswordAPI(BaseRpcView):
|
|||
result['ok'] = ok
|
||||
return result, status.HTTP_200_OK
|
||||
|
||||
|
||||
validate_password = ValidatePasswordAPI.as_view()
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import six
|
||||
|
||||
|
@ -19,6 +35,7 @@ class Setting(object):
|
|||
def has_default(self):
|
||||
return self.default != self.SENTINEL
|
||||
|
||||
|
||||
class AppSettings(object):
|
||||
def __init__(self, defaults):
|
||||
self.defaults = defaults
|
||||
|
@ -35,6 +52,7 @@ class AppSettings(object):
|
|||
realms = {}
|
||||
if self.A2_REGISTRATION_REALM:
|
||||
realms[self.A2_REGISTRATION_REALM] = self.A2_REGISTRATION_REALM
|
||||
|
||||
def add_realms(new_realms):
|
||||
for realm in new_realms:
|
||||
if not isinstance(realm, (tuple, list)):
|
||||
|
@ -68,120 +86,198 @@ class AppSettings(object):
|
|||
return getattr(self.settings, other_key)
|
||||
if self.defaults[key].has_default():
|
||||
return self.defaults[key].default
|
||||
raise ImproperlyConfigured('missing setting %s(%s) is mandatory' %
|
||||
(key, self.defaults[key].description))
|
||||
raise ImproperlyConfigured(
|
||||
'missing setting %s(%s) is mandatory' % (key, self.defaults[key].description))
|
||||
|
||||
|
||||
# Registration
|
||||
default_settings = dict(
|
||||
ATTRIBUTE_BACKENDS = Setting(
|
||||
ATTRIBUTE_BACKENDS=Setting(
|
||||
names=('A2_ATTRIBUTE_BACKENDS',),
|
||||
default=('authentic2.attributes_ng.sources.format',
|
||||
'authentic2.attributes_ng.sources.function',
|
||||
'authentic2.attributes_ng.sources.django_user',
|
||||
'authentic2.attributes_ng.sources.ldap',
|
||||
'authentic2.attributes_ng.sources.computed_targeted_id',
|
||||
'authentic2.attributes_ng.sources.service_roles',
|
||||
default=(
|
||||
'authentic2.attributes_ng.sources.format',
|
||||
'authentic2.attributes_ng.sources.function',
|
||||
'authentic2.attributes_ng.sources.django_user',
|
||||
'authentic2.attributes_ng.sources.ldap',
|
||||
'authentic2.attributes_ng.sources.computed_targeted_id',
|
||||
'authentic2.attributes_ng.sources.service_roles',
|
||||
),
|
||||
definition='List of attribute backend classes or modules',
|
||||
),
|
||||
CAFILE = Setting(names=('AUTHENTIC2_CAFILE', 'CAFILE'),
|
||||
default=None,
|
||||
definition='File containing certificate chains as PEM certificates'),
|
||||
A2_REGISTRATION_URLCONF = Setting(default='authentic2.registration_backend.urls',
|
||||
definition='Root urlconf for the /accounts endpoints'),
|
||||
A2_REGISTRATION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationForm',
|
||||
definition='Default registration form'),
|
||||
A2_REGISTRATION_COMPLETION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationCompletionForm',
|
||||
definition='Default registration completion form'),
|
||||
A2_REGISTRATION_SET_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.SetPasswordForm',
|
||||
definition='Default set password form'),
|
||||
A2_REGISTRATION_CHANGE_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.PasswordChangeForm',
|
||||
definition='Default change password form'),
|
||||
A2_REGISTRATION_CAN_DELETE_ACCOUNT = Setting(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'),
|
||||
A2_REGISTRATION_EMAIL_BLACKLIST = Setting(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'),
|
||||
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_EMAIL_IS_UNIQUE = Setting(default=False,
|
||||
CAFILE=Setting(
|
||||
names=('AUTHENTIC2_CAFILE', 'CAFILE'),
|
||||
default=None,
|
||||
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'),
|
||||
A2_REGISTRATION_CAN_CHANGE_PASSWORD=Setting(
|
||||
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$'),
|
||||
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'),
|
||||
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_EMAIL_IS_UNIQUE=Setting(
|
||||
default=False,
|
||||
definition='Email of users must be unique'),
|
||||
A2_REGISTRATION_EMAIL_IS_UNIQUE = Setting(default=False,
|
||||
A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting(
|
||||
default=False,
|
||||
definition='Email of registererd accounts must be unique'),
|
||||
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting(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')),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
A2_REGISTRATION_USERNAME_IS_UNIQUE=Setting(default=True, definition='Check username uniqueness on registration'),
|
||||
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting(
|
||||
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')),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
A2_REGISTRATION_USERNAME_IS_UNIQUE=Setting(
|
||||
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_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'),
|
||||
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'),
|
||||
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'),
|
||||
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'),
|
||||
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_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'),
|
||||
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'),
|
||||
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'),
|
||||
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(
|
||||
default=None,
|
||||
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'),
|
||||
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'),
|
||||
A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
|
||||
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'),
|
||||
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR=Setting(default=1.8,
|
||||
definition='exponential backoff factor duration as seconds until '
|
||||
'next try after a login failure'),
|
||||
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION=Setting(default=0,
|
||||
definition='exponential backoff base factor duration as secondss '
|
||||
'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'),
|
||||
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'),
|
||||
A2_CORS_WHITELIST=Setting(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'),
|
||||
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(
|
||||
default=False,
|
||||
definition='Show last character in password fields'),
|
||||
A2_AUTH_PASSWORD_ENABLE=Setting(
|
||||
default=True,
|
||||
definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
|
||||
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'),
|
||||
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR=Setting(
|
||||
default=1.8,
|
||||
definition='exponential backoff factor duration as seconds until '
|
||||
'next try after a login failure'),
|
||||
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION=Setting(
|
||||
default=0,
|
||||
definition='exponential backoff base factor duration as secondss '
|
||||
'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'),
|
||||
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'),
|
||||
A2_CORS_WHITELIST=Setting(
|
||||
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'),
|
||||
A2_REDIRECT_WHITELIST=Setting(
|
||||
default=(),
|
||||
definition='List of origins which are authorized to ask for redirection.'),
|
||||
|
@ -199,17 +295,22 @@ default_settings = dict(
|
|||
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'),
|
||||
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'),
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
app_settings = AppSettings(default_settings)
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import re
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -24,7 +39,6 @@ class Authentic2Config(AppConfig):
|
|||
else:
|
||||
expected_type = 'TEXT'
|
||||
|
||||
|
||||
def convert_column_to_json(model, column_name):
|
||||
table_name = model._meta.db_table
|
||||
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import string
|
||||
import datetime
|
||||
import io
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
@ -15,7 +30,6 @@ from django.utils.translation import ugettext_lazy as _, pgettext_lazy
|
|||
from django.utils.functional import allow_lazy
|
||||
from django.utils import html
|
||||
from django.template.defaultfilters import capfirst
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -65,8 +79,9 @@ class BirthdateRestField(serializers.DateField):
|
|||
def get_title_choices():
|
||||
return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES
|
||||
|
||||
validate_phone_number = RegexValidator('^\+?\d{,20}$', message=_('Phone number can start with a + '
|
||||
'an must contain only digits.'))
|
||||
validate_phone_number = RegexValidator(
|
||||
r'^\+?\d{,20}$',
|
||||
message=_('Phone number can start with a + an must contain only digits.'))
|
||||
|
||||
|
||||
class PhoneNumberField(forms.CharField):
|
||||
|
@ -77,7 +92,7 @@ class PhoneNumberField(forms.CharField):
|
|||
|
||||
def clean(self, value):
|
||||
if value not in self.empty_values:
|
||||
value = re.sub('[-.\s]', '', value)
|
||||
value = re.sub(r'[-.\s]', '', value)
|
||||
validate_phone_number(value)
|
||||
return value
|
||||
|
||||
|
@ -87,7 +102,8 @@ class PhoneNumberDRFField(serializers.CharField):
|
|||
|
||||
|
||||
validate_fr_postcode = RegexValidator(
|
||||
'^\d{5}$', message=_('The value must be a valid french postcode'))
|
||||
r'^\d{5}$',
|
||||
message=_('The value must be a valid french postcode'))
|
||||
|
||||
|
||||
class FrPostcodeField(forms.CharField):
|
||||
|
@ -253,7 +269,7 @@ def only_digits(value):
|
|||
|
||||
|
||||
def validate_lun(value):
|
||||
l = [(int(x) * (1 + i % 2)) for i, x in enumerate(reversed(value))]
|
||||
l = [(int(x) * (1 + i % 2)) for i, x in enumerate(reversed(value))] # noqa: E741
|
||||
return sum(x - 9 if x > 10 else x for x in l) % 10 == 0
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from ..decorators import to_iter, to_list
|
||||
|
@ -22,6 +37,7 @@ class UnsortableError(Exception):
|
|||
def __str__(self):
|
||||
return 'UnsortableError: %r' % self.unsortable_instances
|
||||
|
||||
|
||||
def topological_sort(source_and_instances, ctx, raise_on_unsortable=False):
|
||||
'''
|
||||
Sort instances topologically based on their dependency declarations.
|
||||
|
@ -40,9 +56,9 @@ def topological_sort(source_and_instances, ctx, raise_on_unsortable=False):
|
|||
else:
|
||||
new_unsorted.append((source, instance))
|
||||
unsorted = new_unsorted
|
||||
if len(sorted_list) == len(source_and_instances): # finished !
|
||||
if len(sorted_list) == len(source_and_instances): # finished !
|
||||
break
|
||||
elif count_sorted == len(sorted_list): # no progress !
|
||||
elif count_sorted == len(sorted_list): # no progress !
|
||||
if raise_on_unsortable:
|
||||
raise UnsortableError(sorted_list, unsorted)
|
||||
else:
|
||||
|
@ -50,12 +66,12 @@ 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():
|
||||
'''
|
||||
|
@ -68,6 +84,7 @@ def get_sources():
|
|||
for path in plugin.get_attribute_backends():
|
||||
yield utils.import_module_or_class(path)
|
||||
|
||||
|
||||
@to_list
|
||||
def get_attribute_names(ctx):
|
||||
'''
|
||||
|
@ -88,8 +105,7 @@ def get_attributes(ctx):
|
|||
'''
|
||||
source_and_instances = []
|
||||
for source in get_sources():
|
||||
source_and_instances.extend(((source, instance) for instance in
|
||||
source.get_instances(ctx)))
|
||||
source_and_instances.extend(((source, instance) for instance in source.get_instances(ctx)))
|
||||
source_and_instances = topological_sort(source_and_instances, ctx)
|
||||
ctx = ctx.copy()
|
||||
for source, instance in source_and_instances:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
|
||||
from django.utils import six
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Compute a targeted id based on a hash of existing attributes, to compute a
|
||||
targetd id for a service provider and a user coming from an LDAP store using
|
||||
|
@ -24,19 +40,17 @@ AUTHORIZED_KEYS = set(('name', 'label', 'source_attributes', 'salt', 'hash'))
|
|||
|
||||
REQUIRED_KEYS = set(('name', 'source_attributes', 'salt'))
|
||||
|
||||
UNEXPECTED_KEYS_ERROR = \
|
||||
'{0}: unexpected key(s) {1} in configuration'
|
||||
MISSING_KEYS_ERROR = \
|
||||
'{0}: missing key(s) {1} in configuration'
|
||||
BAD_CONFIG_ERROR = \
|
||||
'{0}: template attribute source must contain a name, a list of dependencies and a function'
|
||||
NOT_CALLABLE_ERROR = \
|
||||
'{0}: function attribute must be callable'
|
||||
UNEXPECTED_KEYS_ERROR = '{0}: unexpected key(s) {1} in configuration'
|
||||
MISSING_KEYS_ERROR = '{0}: missing key(s) {1} in configuration'
|
||||
BAD_CONFIG_ERROR = '{0}: template attribute source must contain a name, a list of dependencies and a function'
|
||||
NOT_CALLABLE_ERROR = '{0}: function attribute must be callable'
|
||||
SOURCE_ATTRIBUTE_TYPE_ERROR = '{0}: source_attributes must be a list of string'
|
||||
|
||||
|
||||
def config_error(fmt, *args):
|
||||
raise ImproperlyConfigured(fmt.format(__name__, *args))
|
||||
|
||||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
|
@ -64,9 +78,11 @@ def get_attribute_names(instance, ctx):
|
|||
name = instance['name']
|
||||
return ((name, instance.get('label', name)),)
|
||||
|
||||
|
||||
def get_dependencies(instance, ctx):
|
||||
return instance['source_attributes']
|
||||
|
||||
|
||||
def get_attributes(instance, ctx):
|
||||
source_attributes = instance['source_attributes']
|
||||
source_attributes_values = []
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -6,7 +23,6 @@ from django_rbac.utils import get_role_model
|
|||
from ...models import Attribute, AttributeValue
|
||||
|
||||
from ...decorators import to_list
|
||||
from ...compat import get_user_model
|
||||
|
||||
|
||||
@to_list
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import six
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -6,30 +22,29 @@ from ...decorators import to_list
|
|||
|
||||
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)
|
||||
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'
|
||||
TYPE_ERROR = \
|
||||
'template attribute must be a string'
|
||||
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'
|
||||
TYPE_ERROR = 'template attribute must be a string'
|
||||
|
||||
|
||||
def config_error(fmt, *args):
|
||||
raise ImproperlyConfigured(fmt.format(__name__, *args))
|
||||
|
||||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
|
@ -54,8 +69,10 @@ def get_attribute_names(instance, ctx):
|
|||
name = instance['name']
|
||||
return ((name, instance.get('label', name)),)
|
||||
|
||||
|
||||
def get_dependencies(instance, ctx):
|
||||
return get_field_refs(instance['template'])
|
||||
|
||||
|
||||
def get_attributes(instance, ctx):
|
||||
return {instance['name']: instance['template'].format(**ctx)}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from ...decorators import to_list
|
||||
|
@ -6,19 +22,17 @@ AUTHORIZED_KEYS = set(('name', 'label', 'dependencies', 'function'))
|
|||
|
||||
REQUIRED_KEYS = set(('name', 'dependencies', 'function'))
|
||||
|
||||
UNEXPECTED_KEYS_ERROR = \
|
||||
'{0}: unexpected key(s) {1} in configuration'
|
||||
MISSING_KEYS_ERROR = \
|
||||
'{0}: missing key(s) {1} in configuration'
|
||||
BAD_CONFIG_ERROR = \
|
||||
'{0}: template attribute source must contain a name, a list of dependencies and a function'
|
||||
NOT_CALLABLE_ERROR = \
|
||||
'{0}: function attribute must be callable'
|
||||
UNEXPECTED_KEYS_ERROR = '{0}: unexpected key(s) {1} in configuration'
|
||||
MISSING_KEYS_ERROR = '{0}: missing key(s) {1} in configuration'
|
||||
BAD_CONFIG_ERROR = '{0}: template attribute source must contain a name, a list of dependencies and a function'
|
||||
NOT_CALLABLE_ERROR = '{0}: function attribute must be callable'
|
||||
DEPENDENCY_TYPE_ERROR = '{0}: dependencies must be a list of string'
|
||||
|
||||
|
||||
def config_error(fmt, *args):
|
||||
raise ImproperlyConfigured(fmt.format(__name__, *args))
|
||||
|
||||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
|
@ -40,7 +54,6 @@ def get_instances(ctx):
|
|||
not all(map(lambda x: isinstance(x, str), dependencies)):
|
||||
config_error(DEPENDENCY_TYPE_ERROR)
|
||||
|
||||
|
||||
if not callable(d['function']):
|
||||
config_error(NOT_CALLABLE_ERROR)
|
||||
yield d
|
||||
|
@ -50,9 +63,11 @@ def get_attribute_names(instance, ctx):
|
|||
name = instance['name']
|
||||
return ((name, instance.get('label', name)),)
|
||||
|
||||
|
||||
def get_dependencies(instance, ctx):
|
||||
return instance.get('dependencies', ())
|
||||
|
||||
|
||||
def get_attributes(instance, ctx):
|
||||
args = instance.get('args', ())
|
||||
kwargs = instance.get('kwargs', {})
|
||||
|
|
|
@ -1,7 +1,24 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ...decorators import to_list
|
||||
|
||||
from authentic2.backends.ldap_backend import LDAPBackend, LDAPUser
|
||||
|
||||
|
||||
@to_list
|
||||
def get_instances(ctx):
|
||||
'''
|
||||
|
@ -9,12 +26,15 @@ def get_instances(ctx):
|
|||
'''
|
||||
return [None]
|
||||
|
||||
|
||||
def get_attribute_names(instance, ctx):
|
||||
return LDAPBackend.get_attribute_names()
|
||||
|
||||
|
||||
def get_dependencies(instance, ctx):
|
||||
return ('user',)
|
||||
|
||||
|
||||
def get_attributes(instance, ctx):
|
||||
user = ctx.get('user')
|
||||
if user and isinstance(user, LDAPUser):
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ...models import Service
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import app_settings
|
||||
|
@ -5,9 +22,8 @@ class Plugin(object):
|
|||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
return required(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[
|
||||
url(r'^accounts/sslauth/', include(__name__ + '.urls'))])
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[url(r'^accounts/sslauth/', include(__name__ + '.urls'))])
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
|
|
@ -1,7 +1,24 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class ClientCertificateAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'subject_dn', 'issuer_dn', 'serial')
|
||||
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
|
@ -6,16 +20,16 @@ import sys
|
|||
class AppSettings(object):
|
||||
'''Thanks django-allauth'''
|
||||
__DEFAULTS = dict(
|
||||
# settings for TEST only, make it easy to simulate the SSL
|
||||
# environment
|
||||
ENABLE=False,
|
||||
FORCE_ENV={},
|
||||
ACCEPT_SELF_SIGNED=False,
|
||||
STRICT_MATCH=False,
|
||||
SUBJECT_MATCH_KEYS=('subject_dn', 'issuer_dn'),
|
||||
CREATE_USERNAME_CALLBACK=None,
|
||||
USE_COOKIE=False,
|
||||
CREATE_USER=False,
|
||||
# settings for TEST only, make it easy to simulate the SSL
|
||||
# environment
|
||||
ENABLE=False,
|
||||
FORCE_ENV={},
|
||||
ACCEPT_SELF_SIGNED=False,
|
||||
STRICT_MATCH=False,
|
||||
SUBJECT_MATCH_KEYS=('subject_dn', 'issuer_dn'),
|
||||
CREATE_USERNAME_CALLBACK=None,
|
||||
USE_COOKIE=False,
|
||||
CREATE_USER=False,
|
||||
)
|
||||
|
||||
def __init__(self, prefix):
|
||||
|
@ -23,7 +37,7 @@ class AppSettings(object):
|
|||
|
||||
def _setting(self, name, dflt):
|
||||
from django.conf import settings
|
||||
return getattr(settings, self.prefix+name, dflt)
|
||||
return getattr(settings, self.prefix + name, dflt)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self.__DEFAULTS:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import django.forms
|
||||
|
||||
|
|
|
@ -1,13 +1,31 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
import logging
|
||||
|
||||
from authentic2.compat import get_user_model
|
||||
from authentic2.backends import is_user_authenticable
|
||||
|
||||
from . import models, app_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
@ -39,7 +57,6 @@ class SSLBackend:
|
|||
simply return the user object. That way, we only need top look-up the
|
||||
certificate once, when loggin in
|
||||
"""
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
|
@ -80,7 +97,6 @@ class SSLBackend:
|
|||
just a subject for the ClientCertificate.
|
||||
"""
|
||||
# auto creation only created a DN for the subject, not the issuer
|
||||
User = get_user_model()
|
||||
|
||||
# get username and check if the user exists already
|
||||
if app_settings.CREATE_USERNAME_CALLBACK:
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
from django.contrib.auth import authenticate, login
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
||||
from . import util, app_settings
|
||||
|
||||
|
@ -11,7 +26,7 @@ class SSLAuthMiddleware(object):
|
|||
def process_request(self, request):
|
||||
if app_settings.USE_COOKIE and request.user.is_authenticated():
|
||||
return
|
||||
ssl_info = util.SSLInfo(request)
|
||||
ssl_info = util.SSLInfo(request)
|
||||
user = authenticate(ssl_info=ssl_info)
|
||||
if user and request.user != user:
|
||||
login(request, user)
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import six
|
||||
|
||||
from . import util
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class ClientCertificate(models.Model):
|
||||
serial = models.CharField(max_length=255, blank=True)
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
from .views import (handle_request, post_account_linking, delete_certificate,
|
||||
error_ssl)
|
||||
from .views import (handle_request, post_account_linking, delete_certificate, error_ssl)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$',
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import six
|
||||
|
||||
|
@ -11,25 +27,28 @@ X509_KEYS = {
|
|||
'verify': 'SSL_CLIENT_VERIFY',
|
||||
}
|
||||
|
||||
|
||||
def normalize_cert(certificate_pem):
|
||||
'''Normalize content of the certificate'''
|
||||
base64_content = ''.join(certificate_pem.splitlines()[1:-1])
|
||||
content = base64.b64decode(base64_content)
|
||||
return base64.b64encode(content)
|
||||
|
||||
|
||||
def explode_dn(dn):
|
||||
'''Extract sub element of a DN as displayed by mod_ssl or nginx_ssl'''
|
||||
dn = dn.strip('/')
|
||||
parts = dn.split('/')
|
||||
parts = [part.split('=') for part in parts]
|
||||
parts = [(part[0], part[1].decode('string_escape').decode('utf-8'))
|
||||
for part in parts]
|
||||
parts = [(part[0], part[1].decode('string_escape').decode('utf-8')) for part in parts]
|
||||
return parts
|
||||
|
||||
|
||||
TRANSFORM = {
|
||||
'cert': normalize_cert,
|
||||
'cert': normalize_cert,
|
||||
}
|
||||
|
||||
|
||||
class SSLInfo(object):
|
||||
"""
|
||||
Encapsulates the SSL environment variables in a read-only object. It
|
||||
|
@ -48,7 +67,7 @@ class SSLInfo(object):
|
|||
else:
|
||||
raise EnvironmentError('The SSL authentication currently only \
|
||||
works with mod_python or wsgi requests')
|
||||
self.read_env(env);
|
||||
self.read_env(env)
|
||||
pass
|
||||
|
||||
def read_env(self, env):
|
||||
|
@ -64,7 +83,6 @@ class SSLInfo(object):
|
|||
else:
|
||||
self.__dict__[attr] = None
|
||||
|
||||
|
||||
if self.__dict__['verify'] == 'SUCCESS':
|
||||
self.__dict__['verify'] = True
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -16,24 +32,21 @@ from . import models, util, app_settings
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_request(request):
|
||||
# Check certificate validity
|
||||
ssl_info = util.SSLInfo(request)
|
||||
ssl_info = util.SSLInfo(request)
|
||||
accept_self_signed = app_settings.ACCEPT_SELF_SIGNED
|
||||
|
||||
if not ssl_info.cert:
|
||||
logger.error('SSL Client Authentication failed: '
|
||||
'SSL CGI variable CERT is missing')
|
||||
logger.error('SSL Client Authentication failed: SSL CGI variable CERT is missing')
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('SSL Client Authentication failed. '
|
||||
'No client certificate found.'))
|
||||
_('SSL Client Authentication failed. No client certificate found.'))
|
||||
return redirect_to_login(request)
|
||||
elif not accept_self_signed and not ssl_info.verify:
|
||||
logger.error('SSL Client Authentication failed: '
|
||||
'SSL CGI variable VERIFY is not SUCCESS')
|
||||
logger.error('SSL Client Authentication failed: SSL CGI variable VERIFY is not SUCCESS')
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('SSL Client Authentication failed. '
|
||||
'Your client certificate is not valid.'))
|
||||
_('SSL Client Authentication failed. Your client certificate is not valid.'))
|
||||
return redirect_to_login(request)
|
||||
|
||||
# SSL entries for this certificate?
|
||||
|
@ -51,7 +64,7 @@ def handle_request(request):
|
|||
else:
|
||||
logger.error('account creation failure')
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('SSL Client Authentication failed. Internal server error.'))
|
||||
_('SSL Client Authentication failed. Internal server error.'))
|
||||
return redirect_to_login(request)
|
||||
|
||||
# No SSL entries and no user session, redirect account linking page
|
||||
|
@ -61,14 +74,12 @@ def handle_request(request):
|
|||
# No SSL entries but active user session, perform account linking
|
||||
if not user and request.user.is_authenticated():
|
||||
from backend import SSLBackend
|
||||
if SSLBackend().link_user(ssl_info, request.user):
|
||||
logger.info('Successful linking of the SSL '
|
||||
'Certificate to an account, redirection to %s' % next_url)
|
||||
else:
|
||||
if not SSLBackend().link_user(ssl_info, request.user):
|
||||
logger.error('login() failed')
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('SSL Client Authentication failed. Internal server error.'))
|
||||
_('SSL Client Authentication failed. Internal server error.'))
|
||||
return redirect_to_login(request)
|
||||
logger.info('Successful linking of the SSL Certificate to an account')
|
||||
|
||||
# SSL Entries found for this certificate,
|
||||
# if the user is logged out, we login
|
||||
|
@ -81,56 +92,40 @@ def handle_request(request):
|
|||
# check that the SSL entry for the certificate is this user.
|
||||
# else, we make this certificate point on that user.
|
||||
if user.username != request.user.username:
|
||||
logger.warning(u'The certificate belongs to %s, '
|
||||
'but %s is logged with, we change the association!',
|
||||
user, request.user)
|
||||
logger.warning(u'The certificate belongs to %s, but %s is logged with, we change the association!',
|
||||
user, request.user)
|
||||
from backends import SSLBackend
|
||||
cert = SSLBackend().get_certificate(ssl_info)
|
||||
cert.user = request.user
|
||||
cert.save()
|
||||
return continue_to_next_url(request)
|
||||
|
||||
###
|
||||
# post_account_linking
|
||||
# @request
|
||||
#
|
||||
# Called after an account linking.
|
||||
###
|
||||
|
||||
@csrf_exempt
|
||||
def post_account_linking(request):
|
||||
logger.info('auth2_ssl Return after account linking form filled')
|
||||
if request.method == "POST":
|
||||
if 'do_creation' in request.POST \
|
||||
and request.POST['do_creation'] == 'on':
|
||||
logger.info('account creation asked')
|
||||
if 'do_creation' in request.POST and request.POST['do_creation'] == 'on':
|
||||
request.session['do_creation'] = 'do_creation'
|
||||
return redirect_to_login(request, login_url='user_signin_ssl')
|
||||
form = AuthenticationForm(data=request.POST)
|
||||
if form.is_valid():
|
||||
logger.info('form valid')
|
||||
user = form.get_user()
|
||||
try:
|
||||
login(request, user)
|
||||
record_authentication_event(request, how='password')
|
||||
except:
|
||||
logger.error('login() failed')
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('SSL Client Authentication failed. Internal server error.'))
|
||||
|
||||
logger.debug('session opened')
|
||||
login(request, user)
|
||||
record_authentication_event(request, how='password')
|
||||
return redirect_to_login(request, login_url='user_signin_ssl')
|
||||
else:
|
||||
logger.warning('form not valid - Try again! (Brute force?)')
|
||||
return render(request, 'auth/account_linking_ssl.html')
|
||||
else:
|
||||
return render(request, 'auth/account_linking_ssl.html')
|
||||
|
||||
|
||||
def profile(request, template_name='ssl/profile.html', *args, **kwargs):
|
||||
context = kwargs.pop('context', {})
|
||||
certificates = models.ClientCertificate.objects.filter(user=request.user)
|
||||
context.update({'certificates': certificates})
|
||||
return render_to_string(template_name, context, request=request)
|
||||
|
||||
|
||||
def delete_certificate(request, certificate_pk):
|
||||
qs = models.ClientCertificate.objects.filter(pk=certificate_pk)
|
||||
count = qs.count()
|
||||
|
@ -138,8 +133,8 @@ def delete_certificate(request, certificate_pk):
|
|||
if count:
|
||||
logger.info('client certificate %s deleted', certificate_pk)
|
||||
messages.info(request, _('Certificate deleted.'))
|
||||
return redirect(request, 'account_management',
|
||||
fragment='a2-ssl-certificate-profile')
|
||||
return redirect(request, 'account_management', fragment='a2-ssl-certificate-profile')
|
||||
|
||||
|
||||
class SslErrorView(TemplateView):
|
||||
template_name = 'error_ssl.html'
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from authentic2_idp_oidc.models import OIDCClient
|
||||
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
|
|
@ -1,7 +1,24 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from . import views, app_settings, utils, constants, forms
|
||||
from . import views, app_settings, utils, constants
|
||||
from .forms import authentication as authentication_forms
|
||||
|
||||
|
||||
class LoginPasswordAuthenticator(object):
|
||||
|
@ -20,7 +37,7 @@ class LoginPasswordAuthenticator(object):
|
|||
context = kwargs.get('context', {})
|
||||
is_post = request.method == 'POST' and self.submit_name in request.POST
|
||||
data = request.POST if is_post else None
|
||||
form = forms.AuthenticationForm(request=request, data=data)
|
||||
form = authentication_forms.AuthenticationForm(request=request, data=data)
|
||||
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:
|
||||
form.fields['username'].label = _('Username or email')
|
||||
if app_settings.A2_USERNAME_LABEL:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from authentic2 import app_settings
|
||||
|
||||
|
@ -25,5 +41,5 @@ def is_user_authenticable(user):
|
|||
return get_user_queryset().filter(pk=user.pk).exists()
|
||||
|
||||
|
||||
from .ldap_backend import LDAPBackend
|
||||
from .models_backend import ModelBackend
|
||||
from .ldap_backend import LDAPBackend # noqa: F401
|
||||
from .models_backend import ModelBackend # noqa: F401
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import ldap
|
||||
import ldap.modlist
|
||||
|
@ -19,23 +35,19 @@ import os
|
|||
# code originaly copied from by now merely inspired by
|
||||
# http://www.amherst.k12.oh.us/django-ldap.html
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.encoding import force_bytes, force_text
|
||||
from django.utils import six
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils import six
|
||||
|
||||
from authentic2.a2_rbac.models import Role
|
||||
|
||||
from authentic2.compat_lasso import lasso
|
||||
|
||||
from authentic2 import crypto, app_settings
|
||||
from authentic2.decorators import to_list
|
||||
from authentic2.compat import get_user_model
|
||||
from authentic2.models import UserExternalId
|
||||
from authentic2.middleware import StoreRequestMiddleware
|
||||
from authentic2.user_login_failure import user_login_failure, user_login_success
|
||||
|
@ -46,6 +58,9 @@ from authentic2.utils import utf8_encode, to_list
|
|||
|
||||
from authentic2.backends import is_user_authenticable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
DEFAULT_CA_BUNDLE = ''
|
||||
|
||||
|
@ -212,7 +227,7 @@ def map_text(d):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class LDAPUser(get_user_model()):
|
||||
class LDAPUser(User):
|
||||
SESSION_LDAP_DATA_KEY = 'ldap-data'
|
||||
_changed = False
|
||||
|
||||
|
@ -261,12 +276,12 @@ class LDAPUser(get_user_model()):
|
|||
def update_request(self):
|
||||
request = StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
assert not request.session is None
|
||||
assert request.session is not None
|
||||
self.init_to_session(request.session)
|
||||
|
||||
def init_from_request(self):
|
||||
request = StoreRequestMiddleware.get_request()
|
||||
assert request and not request.session is None
|
||||
assert request and request.session is not None
|
||||
self.init_from_session(request.session)
|
||||
|
||||
def keep_password(self, password):
|
||||
|
@ -377,7 +392,8 @@ class LDAPBackend(object):
|
|||
'bindpw': '',
|
||||
'bindsasl': (),
|
||||
'user_dn_template': '',
|
||||
'user_filter': 'uid=%s', # will be '(|(mail=%s)(uid=%s))' if A2_ACCEPT_EMAIL_AUTHENTICATION is set (see update_default)
|
||||
'user_filter': 'uid=%s', # will be '(|(mail=%s)(uid=%s))' if
|
||||
# A2_ACCEPT_EMAIL_AUTHENTICATION is set (see update_default)
|
||||
'sync_ldap_users_filter': '',
|
||||
'user_basedn': '',
|
||||
'group_dn_template': '',
|
||||
|
@ -586,7 +602,7 @@ class LDAPBackend(object):
|
|||
if not block['connect_with_user_credentials']:
|
||||
try:
|
||||
self.bind(block, conn)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
log.exception(u'rebind failure after login bind')
|
||||
raise ldap.SERVER_DOWN
|
||||
break
|
||||
|
@ -739,8 +755,7 @@ class LDAPBackend(object):
|
|||
for role_name in role_names:
|
||||
role, error = self.get_role(block, role_id=role_name)
|
||||
if role is None:
|
||||
log.warning('error %s: couldn\'t retrieve role %r',
|
||||
error, role_name)
|
||||
log.warning('error %s: couldn\'t retrieve role %r', error, role_name)
|
||||
continue
|
||||
# Add missing roles
|
||||
if dn in role_dns and role not in roles:
|
||||
|
@ -842,7 +857,6 @@ class LDAPBackend(object):
|
|||
if group not in groups:
|
||||
user.groups.add(group)
|
||||
|
||||
|
||||
def populate_mandatory_roles(self, user, block):
|
||||
mandatory_roles = block.get('set_mandatory_roles')
|
||||
if not mandatory_roles:
|
||||
|
@ -854,8 +868,7 @@ class LDAPBackend(object):
|
|||
for role_name in mandatory_roles:
|
||||
role, error = self.get_role(block, role_id=role_name)
|
||||
if role is None:
|
||||
log.warning('error %s: couldn\'t retrieve role %r',
|
||||
error, role_name)
|
||||
log.warning('error %s: couldn\'t retrieve role %r', error, role_name)
|
||||
continue
|
||||
if role not in roles:
|
||||
user.roles.add(role)
|
||||
|
@ -996,7 +1009,6 @@ class LDAPBackend(object):
|
|||
return ' '.join(part for part in parts)
|
||||
|
||||
def lookup_by_username(self, username):
|
||||
User = get_user_model()
|
||||
try:
|
||||
log.debug('lookup using username %r', username)
|
||||
return LDAPUser.objects.prefetch_related('groups').get(username=username)
|
||||
|
@ -1004,7 +1016,6 @@ class LDAPBackend(object):
|
|||
return
|
||||
|
||||
def lookup_by_external_id(self, block, attributes):
|
||||
User = get_user_model()
|
||||
for eid_tuple in map_text(block['external_id_tuples']):
|
||||
external_id = self.build_external_id(eid_tuple, attributes)
|
||||
if not external_id:
|
||||
|
@ -1019,7 +1030,7 @@ class LDAPBackend(object):
|
|||
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))
|
||||
len(users))
|
||||
for other in users[1:]:
|
||||
for r in other.roles.all():
|
||||
user.roles.add(r)
|
||||
|
@ -1312,8 +1323,8 @@ class LDAPBackend(object):
|
|||
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))):
|
||||
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)
|
||||
if isinstance(cls._DEFAULTS[d], dict) and not isinstance(block[d], dict):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
|
||||
from django.utils.decorators import method_decorator
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
from datetime import datetime
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import inspect
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.utils import OperationalError
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
|
||||
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
|
||||
|
||||
user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
default_token_generator = PasswordResetTokenGenerator()
|
||||
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import lasso
|
||||
except ImportError:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
NONCE_FIELD_NAME = 'nonce'
|
||||
CANCEL_FIELD_NAME = 'cancel'
|
||||
|
|
|
@ -1,17 +1,32 @@
|
|||
from collections import defaultdict
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pkg_resources import get_distribution
|
||||
from django.conf import settings
|
||||
|
||||
from . import utils, app_settings, constants
|
||||
|
||||
|
||||
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])
|
||||
|
@ -29,6 +44,7 @@ class UserFederations(object):
|
|||
|
||||
__AUTHENTIC2_DISTRIBUTION = None
|
||||
|
||||
|
||||
def a2_processor(request):
|
||||
global __AUTHENTIC2_DISTRIBUTION
|
||||
variables = {}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .decorators import SessionCache
|
||||
|
||||
from django.conf import settings
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import struct
|
||||
|
@ -101,7 +117,8 @@ def aes_base64url_deterministic_encrypt(key, data, salt, hash_name='sha256', cou
|
|||
|
||||
iv = hashmod.new(salt).digest()
|
||||
|
||||
prf = lambda secret, salt: HMAC.new(secret, salt, hashmod).digest()
|
||||
def prf(secret, salt):
|
||||
return HMAC.new(secret, salt, hashmod).digest()
|
||||
|
||||
aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf)
|
||||
|
||||
|
@ -122,7 +139,9 @@ def aes_base64url_deterministic_decrypt(key, urlencoded, salt, raise_on_error=Tr
|
|||
hashmod = SHA256
|
||||
key_size = 16
|
||||
hmac_size = key_size
|
||||
prf = lambda secret, salt: HMAC.new(secret, salt, hashmod).digest()
|
||||
|
||||
def prf(secret, salt):
|
||||
return HMAC.new(secret, salt, hashmod).digest()
|
||||
|
||||
try:
|
||||
try:
|
||||
|
|
|
@ -1 +1,17 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
default_app_config = 'authentic2.custom_user.apps.CustomUserConfig'
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db import DEFAULT_DB_ALIAS, router
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
import getpass
|
||||
|
@ -35,7 +51,7 @@ class Command(BaseCommand):
|
|||
UserModel = get_user_model()
|
||||
|
||||
qs = UserModel._default_manager.using(options.get('database'))
|
||||
qs = qs.filter(Q(uuid=username)|Q(username=username)|Q(email=username))
|
||||
qs = qs.filter(Q(uuid=username) | Q(username=username) | Q(email=username))
|
||||
try:
|
||||
u = qs.get()
|
||||
except UserModel.DoesNotExist:
|
||||
|
@ -44,18 +60,18 @@ class Command(BaseCommand):
|
|||
while True:
|
||||
print('Select a user:')
|
||||
for i, user in enumerate(qs):
|
||||
print('%d.' % (i+1), user)
|
||||
print('%d.' % (i + 1), user)
|
||||
print('> ', end=' ')
|
||||
try:
|
||||
j = input()
|
||||
except SyntaxError:
|
||||
print('Please enter an integer')
|
||||
continue
|
||||
if not isinstance(uid, int):
|
||||
if not isinstance(j, int):
|
||||
print('Please enter an integer')
|
||||
continue
|
||||
try:
|
||||
u = qs[j-1]
|
||||
u = qs[j - 1]
|
||||
break
|
||||
except IndexError:
|
||||
print('Please enter an integer between 1 and %d' % qs.count())
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
|
@ -13,10 +29,12 @@ class UserQuerySet(models.QuerySet):
|
|||
searchable_attributes = Attribute.objects.filter(searchable=True)
|
||||
queries = []
|
||||
for term in terms:
|
||||
q = (models.query.Q(username__icontains=term) |
|
||||
models.query.Q(first_name__icontains=term) |
|
||||
models.query.Q(last_name__icontains=term) |
|
||||
models.query.Q(email__icontains=term))
|
||||
q = (
|
||||
models.query.Q(username__icontains=term)
|
||||
| models.query.Q(first_name__icontains=term)
|
||||
| models.query.Q(last_name__icontains=term)
|
||||
| models.query.Q(email__icontains=term)
|
||||
)
|
||||
for a in searchable_attributes:
|
||||
if a.name in ('first_name', 'last_name'):
|
||||
continue
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.core.mail import send_mail
|
||||
|
@ -85,8 +101,8 @@ 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)
|
||||
v is not None
|
||||
and v == getattr(self.user.verified_attributes, name, None)
|
||||
)
|
||||
|
||||
|
||||
|
@ -103,20 +119,29 @@ 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,
|
||||
validators=[validators.EmailValidator], max_length=254)
|
||||
email = models.EmailField(
|
||||
_('email address'),
|
||||
blank=True,
|
||||
validators=[validators.EmailValidator],
|
||||
max_length=254)
|
||||
email_verified = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('email verified'))
|
||||
is_staff = models.BooleanField(_('staff status'), default=False,
|
||||
is_staff = models.BooleanField(
|
||||
_('staff status'),
|
||||
default=False,
|
||||
help_text=_('Designates whether the user can log into this admin '
|
||||
'site.'))
|
||||
is_active = models.BooleanField(_('active'), default=True,
|
||||
is_active = models.BooleanField(
|
||||
_('active'),
|
||||
default=True,
|
||||
help_text=_('Designates whether this user should be treated as '
|
||||
'active. Unselect this instead of deleting accounts.'))
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from django_rbac.models import Operation
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
import re
|
||||
|
@ -6,15 +22,15 @@ from contextlib import contextmanager
|
|||
import time
|
||||
from functools import wraps
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.debug import technical_404_response
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
|
||||
from django.core.cache import cache as django_cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import six
|
||||
|
||||
from . import utils, app_settings, middleware
|
||||
from .utils import to_list, to_iter
|
||||
from . import app_settings, middleware
|
||||
# XXX: import to_list for retrocompaibility
|
||||
from .utils import to_list, to_iter # noqa: F401
|
||||
|
||||
|
||||
class CacheUnusable(RuntimeError):
|
||||
|
@ -32,23 +48,27 @@ def unless(test, message):
|
|||
return f
|
||||
return decorator
|
||||
|
||||
|
||||
def setting_enabled(name, settings=app_settings):
|
||||
'''Generate a decorator for enabling a view based on a setting'''
|
||||
full_name = getattr(settings, 'prefix', '') + name
|
||||
|
||||
def test():
|
||||
return getattr(settings, name, False)
|
||||
return unless(test, 'please enable %s' % full_name)
|
||||
|
||||
|
||||
def lasso_required():
|
||||
def test():
|
||||
try:
|
||||
import lasso
|
||||
import lasso # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
return unless(test, 'please install lasso')
|
||||
|
||||
def required(wrapping_functions,patterns_rslt):
|
||||
|
||||
def required(wrapping_functions, patterns_rslt):
|
||||
'''
|
||||
Used to require 1..n decorators in any view returned by a url tree
|
||||
|
||||
|
@ -69,36 +89,39 @@ def required(wrapping_functions,patterns_rslt):
|
|||
patterns(...)
|
||||
)
|
||||
'''
|
||||
if not hasattr(wrapping_functions,'__iter__'):
|
||||
if not hasattr(wrapping_functions, '__iter__'):
|
||||
wrapping_functions = (wrapping_functions,)
|
||||
|
||||
return [
|
||||
_wrap_instance__resolve(wrapping_functions,instance)
|
||||
_wrap_instance__resolve(wrapping_functions, instance)
|
||||
for instance in patterns_rslt
|
||||
]
|
||||
|
||||
def _wrap_instance__resolve(wrapping_functions,instance):
|
||||
if not hasattr(instance,'resolve'): return instance
|
||||
resolve = getattr(instance,'resolve')
|
||||
|
||||
def _wrap_func_in_returned_resolver_match(*args,**kwargs):
|
||||
rslt = resolve(*args,**kwargs)
|
||||
def _wrap_instance__resolve(wrapping_functions, instance):
|
||||
if not hasattr(instance, 'resolve'):
|
||||
return instance
|
||||
resolve = getattr(instance, 'resolve')
|
||||
|
||||
if not hasattr(rslt,'func'):return rslt
|
||||
f = getattr(rslt,'func')
|
||||
def _wrap_func_in_returned_resolver_match(*args, **kwargs):
|
||||
rslt = resolve(*args, **kwargs)
|
||||
|
||||
if not hasattr(rslt, 'func'):
|
||||
return rslt
|
||||
f = getattr(rslt, 'func')
|
||||
|
||||
for _f in reversed(wrapping_functions):
|
||||
# @decorate the function from inner to outter
|
||||
f = _f(f)
|
||||
|
||||
setattr(rslt,'func',f)
|
||||
setattr(rslt, 'func', f)
|
||||
|
||||
return rslt
|
||||
|
||||
setattr(instance,'resolve',_wrap_func_in_returned_resolver_match)
|
||||
|
||||
setattr(instance, 'resolve', _wrap_func_in_returned_resolver_match)
|
||||
return instance
|
||||
|
||||
|
||||
class CacheDecoratorBase(object):
|
||||
'''Base class to build cache decorators.
|
||||
|
||||
|
@ -106,8 +129,8 @@ class CacheDecoratorBase(object):
|
|||
'''
|
||||
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__)
|
||||
raise TypeError(
|
||||
'%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])
|
||||
|
@ -139,27 +162,27 @@ 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:
|
||||
return value
|
||||
if (self.timeout is None
|
||||
or tstamp + self.timeout > now):
|
||||
return value
|
||||
if hasattr(self, 'delete'):
|
||||
self.delete(key, (key, tstamp))
|
||||
value = func(*args, **kwargs)
|
||||
self.set(key, (value, now))
|
||||
return value
|
||||
except CacheUnusable: # fallback when cache cannot be used
|
||||
except CacheUnusable: # fallback when cache cannot be used
|
||||
return func(*args, **kwargs)
|
||||
f.cache = self
|
||||
return f
|
||||
|
||||
def key(self, *args, **kwargs):
|
||||
'''Transform arguments to string and build a key from it'''
|
||||
parts = [str(id(self))] # add cache instance to the key
|
||||
parts = [str(id(self))] # add cache instance to the key
|
||||
if self.hostname_vary:
|
||||
request = middleware.StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
parts.append(request.get_host())
|
||||
else:
|
||||
else:
|
||||
# if we cannot determine the hostname it's better to ignore the
|
||||
# cache
|
||||
raise CacheUnusable
|
||||
|
@ -275,6 +298,7 @@ def errorcollector(error_dict):
|
|||
def json(func):
|
||||
'''Convert view to a JSON or JSON web-service supporting CORS'''
|
||||
from . import cors
|
||||
|
||||
@wraps(func)
|
||||
def f(request, *args, **kwargs):
|
||||
jsonp = False
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Discovery Service Responder
|
||||
See Identity Provider Discovery Service Protocol and Profile
|
||||
|
@ -62,17 +78,14 @@ def get_disco_return_url_from_metadata(entity_id):
|
|||
try:
|
||||
liberty_provider = LibertyProvider.objects.get(entity_id=entity_id)
|
||||
liberty_provider.service_provider
|
||||
except:
|
||||
logger.warn("get_disco_return_url_from_metadata: "
|
||||
"unknown service provider %s" \
|
||||
% entity_id)
|
||||
except Exception:
|
||||
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)
|
||||
logger.warn('get_disco_return_url_from_metadata: no discovery service endpoint for %s', entity_id)
|
||||
return None
|
||||
ep = None
|
||||
value = 0
|
||||
|
@ -89,24 +102,17 @@ def get_disco_return_url_from_metadata(entity_id):
|
|||
value = int(endpoint.attributes['index'].value)
|
||||
ep = endpoint
|
||||
if not ep:
|
||||
logger.warn("get_disco_return_url_from_metadata: "
|
||||
"no valid endpoint for %s" \
|
||||
% entity_id)
|
||||
logger.warn("get_disco_return_url_from_metadata: no valid endpoint for %s", entity_id)
|
||||
return None
|
||||
|
||||
logger.debug("get_disco_return_url_from_metadata: "
|
||||
"found endpoint with index %s" \
|
||||
% str(value))
|
||||
logger.debug('get_disco_return_url_from_metadata: found endpoint with index %s', value)
|
||||
|
||||
if 'Location' in ep.attributes.keys():
|
||||
location = ep.attributes['Location'].value
|
||||
logger.debug("get_disco_return_url_from_metadata: "
|
||||
"location is %s" \
|
||||
% location)
|
||||
logger.debug('get_disco_return_url_from_metadata: location is %s', location)
|
||||
return location
|
||||
|
||||
logger.warn("get_disco_return_url_from_metadata: "
|
||||
"no location found for endpoint with index %s" \
|
||||
% str(value))
|
||||
logger.warn('get_disco_return_url_from_metadata: no location found for endpoint with index %s', value)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -145,9 +151,7 @@ def disco(request):
|
|||
|
||||
# Back from the selection interface
|
||||
if idp_selected:
|
||||
logger.info("disco: "
|
||||
"back from the idp selection interface with value %s" \
|
||||
% idp_selected)
|
||||
logger.info('disco: back from the idp selection interface with value %s', idp_selected)
|
||||
|
||||
if not is_known_idp(idp_selected):
|
||||
message = 'The idp is unknown.'
|
||||
|
@ -163,21 +167,20 @@ 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', '')
|
||||
if isPassive and isPassive == 'true':
|
||||
isPassive=True
|
||||
isPassive = True
|
||||
else:
|
||||
isPAssive=False
|
||||
isPassive = False
|
||||
|
||||
if not entityID:
|
||||
message = _('missing mandatory parameter entityID')
|
||||
return error_page(request, message, logger=logger)
|
||||
|
||||
if policy != \
|
||||
'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single':
|
||||
if policy != 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single':
|
||||
message = _('policy %r not implemented') % policy
|
||||
return error_page(request, message, logger=logger)
|
||||
|
||||
|
@ -189,15 +192,14 @@ def disco(request):
|
|||
else:
|
||||
return_url = _return
|
||||
if not return_url:
|
||||
message = _('unable to find a valid return url for %s' \
|
||||
% entityID)
|
||||
message = _('unable to find a valid return url for %s') % entityID
|
||||
return error_page(request, message, logger=logger)
|
||||
|
||||
# Check that the return_url does not already contain a param with name
|
||||
# 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))
|
||||
message = _('invalid return url %(return_url)s for %(entity_id)s') % dict(
|
||||
return_url=return_url, entity_id=entityID)
|
||||
return error_page(request, message, logger=logger)
|
||||
|
||||
# not back from selection interface
|
||||
|
@ -208,24 +210,22 @@ def disco(request):
|
|||
if not idp_selected:
|
||||
# no idp selected and we must not interect with the user
|
||||
if isPassive:
|
||||
#No IdP selected = just return to the return url
|
||||
# No IdP selected = just return to the return url
|
||||
return HttpResponseRedirect(return_url)
|
||||
# Go to selection interface
|
||||
else:
|
||||
save_key_values(request, entityID, _return, policy, returnIDParam,
|
||||
isPassive)
|
||||
save_key_values(request, entityID, _return, policy, returnIDParam, isPassive)
|
||||
return HttpResponseRedirect(reverse(idp_selection))
|
||||
|
||||
# We got it!
|
||||
set_or_refresh_prefered_idp(request, idp_selected)
|
||||
return HttpResponseRedirect(add_param_to_url(return_url, returnIDParam,
|
||||
idp_selected))
|
||||
return HttpResponseRedirect(add_param_to_url(return_url, returnIDParam, idp_selected))
|
||||
|
||||
|
||||
def idp_selection(request):
|
||||
# XXX: Code here the IdP selection
|
||||
idp_selected = urlquote('http://www.identity-hub.com/idp/saml2/metadata')
|
||||
return HttpResponseRedirect('%s?idp_selected=%s' \
|
||||
% (reverse(disco), idp_selected))
|
||||
return HttpResponseRedirect('%s?idp_selected=%s' % (reverse(disco), idp_selected))
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^disco$', disco),
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import logging
|
||||
import hashlib
|
||||
|
|
|
@ -1,273 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
|
||||
from django import forms
|
||||
from django.forms.models import modelform_factory as django_modelform_factory
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, forms as auth_forms
|
||||
from django.utils import html
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from django_rbac.utils import get_ou_model
|
||||
|
||||
from authentic2.utils import lazy_label
|
||||
from authentic2.compat import get_user_model
|
||||
from authentic2.forms.fields import PasswordField
|
||||
|
||||
from .. import app_settings
|
||||
from ..exponential_retry_timeout import ExponentialRetryTimeout
|
||||
|
||||
OU = get_ou_model()
|
||||
|
||||
|
||||
class EmailChangeFormNoPassword(forms.Form):
|
||||
email = forms.EmailField(label=_('New email'))
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class EmailChangeForm(EmailChangeFormNoPassword):
|
||||
password = forms.CharField(label=_("Password"),
|
||||
widget=forms.PasswordInput)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
if email == self.user.email:
|
||||
raise forms.ValidationError(_('This is already your email address.'))
|
||||
return email
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data["password"]
|
||||
if not self.user.check_password(password):
|
||||
raise forms.ValidationError(
|
||||
_('Incorrect password.'),
|
||||
code='password_incorrect',
|
||||
)
|
||||
return password
|
||||
|
||||
|
||||
class NextUrlFormMixin(forms.Form):
|
||||
next_url = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from authentic2.middleware import StoreRequestMiddleware
|
||||
|
||||
next_url = kwargs.pop('next_url', None)
|
||||
request = StoreRequestMiddleware.get_request()
|
||||
if not next_url and request:
|
||||
next_url = request.GET.get(REDIRECT_FIELD_NAME)
|
||||
super(NextUrlFormMixin, self).__init__(*args, **kwargs)
|
||||
if next_url:
|
||||
self.fields['next_url'].initial = next_url
|
||||
|
||||
|
||||
class BaseUserForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
'duplicate_username': _("A user with that username already exists."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from authentic2 import models
|
||||
|
||||
self.attributes = models.Attribute.objects.all()
|
||||
initial = kwargs.setdefault('initial', {})
|
||||
if kwargs.get('instance'):
|
||||
instance = kwargs['instance']
|
||||
for av in models.AttributeValue.objects.with_owner(instance):
|
||||
if av.attribute.name in self.declared_fields:
|
||||
if av.verified:
|
||||
self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly'
|
||||
initial[av.attribute.name] = av.to_python()
|
||||
super(BaseUserForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
from authentic2 import models
|
||||
|
||||
# make sure verified fields are not modified
|
||||
for av in models.AttributeValue.objects.with_owner(
|
||||
self.instance).filter(verified=True):
|
||||
self.cleaned_data[av.attribute.name] = av.to_python()
|
||||
super(BaseUserForm, self).clean()
|
||||
|
||||
def save_attributes(self):
|
||||
# only save non verified attributes here
|
||||
verified_attributes = set(
|
||||
self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True)
|
||||
)
|
||||
for attribute in self.attributes:
|
||||
name = attribute.name
|
||||
if name in self.fields and name not in verified_attributes:
|
||||
value = self.cleaned_data[name]
|
||||
setattr(self.instance.attributes, name, value)
|
||||
|
||||
def save(self, commit=True):
|
||||
result = super(BaseUserForm, self).save(commit=commit)
|
||||
if commit:
|
||||
self.save_attributes()
|
||||
else:
|
||||
old = self.save_m2m
|
||||
|
||||
def save_m2m(*args, **kwargs):
|
||||
old(*args, **kwargs)
|
||||
self.save_attributes()
|
||||
self.save_m2m = save_m2m
|
||||
return result
|
||||
|
||||
|
||||
class EditProfileForm(NextUrlFormMixin, BaseUserForm):
|
||||
pass
|
||||
|
||||
|
||||
def modelform_factory(model, **kwargs):
|
||||
'''Build a modelform for the given model,
|
||||
|
||||
For the user model also add attribute based fields.
|
||||
'''
|
||||
from authentic2 import models
|
||||
|
||||
form = kwargs.pop('form', None)
|
||||
fields = kwargs.get('fields') or []
|
||||
required = list(kwargs.pop('required', []) or [])
|
||||
d = {}
|
||||
# KV attributes are only supported for the user model currently
|
||||
modelform = None
|
||||
if issubclass(model, get_user_model()):
|
||||
if not form:
|
||||
form = BaseUserForm
|
||||
attributes = models.Attribute.objects.all()
|
||||
for attribute in attributes:
|
||||
if attribute.name not in fields:
|
||||
continue
|
||||
d[attribute.name] = attribute.get_form_field()
|
||||
for field in app_settings.A2_REQUIRED_FIELDS:
|
||||
if field not in required:
|
||||
required.append(field)
|
||||
if not form or not hasattr(form, 'Meta'):
|
||||
meta_d = {'model': model, 'fields': '__all__'}
|
||||
meta = type('Meta', (), meta_d)
|
||||
d['Meta'] = meta
|
||||
if not form: # fallback
|
||||
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 django_modelform_factory(model, **kwargs)
|
||||
|
||||
|
||||
class AuthenticationForm(auth_forms.AuthenticationForm):
|
||||
password = PasswordField(label=_('Password'))
|
||||
remember_me = forms.BooleanField(
|
||||
initial=False,
|
||||
required=False,
|
||||
label=_('Remember me'),
|
||||
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())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AuthenticationForm, self).__init__(*args, **kwargs)
|
||||
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)
|
||||
|
||||
if not app_settings.A2_USER_REMEMBER_ME:
|
||||
del self.fields['remember_me']
|
||||
|
||||
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
||||
del self.fields['ou']
|
||||
|
||||
if self.request:
|
||||
self.remote_addr = self.request.META['REMOTE_ADDR']
|
||||
else:
|
||||
self.remote_addr = '0.0.0.0'
|
||||
|
||||
def exp_backoff_keys(self):
|
||||
return self.cleaned_data['username'], self.remote_addr
|
||||
|
||||
def clean(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
keys = None
|
||||
if username and password:
|
||||
keys = self.exp_backoff_keys()
|
||||
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 = msg % int(math.ceil(seconds_to_wait))
|
||||
msg = html.mark_safe(msg)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
try:
|
||||
self.clean_authenticate()
|
||||
except Exception:
|
||||
if keys:
|
||||
self.exponential_backoff.failure(*keys)
|
||||
raise
|
||||
else:
|
||||
if keys:
|
||||
self.exponential_backoff.success(*keys)
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_authenticate(self):
|
||||
# copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
ou = self.cleaned_data.get('ou')
|
||||
|
||||
if username is not None and password:
|
||||
self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login',
|
||||
params={'username': self.username_field.verbose_name},
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(self.user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = super(AuthenticationForm, self).media
|
||||
media.add_js(['authentic2/js/js_seconds_until.js'])
|
||||
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
||||
media.add_js(['authentic2/js/ou_selector.js'])
|
||||
return media
|
||||
|
||||
|
||||
class SiteImportForm(forms.Form):
|
||||
site_json = forms.FileField(label=_('Site Export File'))
|
|
@ -0,0 +1,119 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import forms as auth_forms
|
||||
from django.utils import html
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from authentic2.forms.fields import PasswordField
|
||||
|
||||
from ..a2_rbac.models import OrganizationalUnit as OU
|
||||
from .. import app_settings, utils
|
||||
from ..exponential_retry_timeout import ExponentialRetryTimeout
|
||||
|
||||
|
||||
class AuthenticationForm(auth_forms.AuthenticationForm):
|
||||
password = PasswordField(label=_('Password'))
|
||||
remember_me = forms.BooleanField(
|
||||
initial=False,
|
||||
required=False,
|
||||
label=_('Remember me'),
|
||||
help_text=_('Do not ask for authentication next time'))
|
||||
ou = forms.ModelChoiceField(
|
||||
label=utils.lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL),
|
||||
required=True,
|
||||
queryset=OU.objects.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AuthenticationForm, self).__init__(*args, **kwargs)
|
||||
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)
|
||||
|
||||
if not app_settings.A2_USER_REMEMBER_ME:
|
||||
del self.fields['remember_me']
|
||||
|
||||
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
||||
del self.fields['ou']
|
||||
|
||||
if self.request:
|
||||
self.remote_addr = self.request.META['REMOTE_ADDR']
|
||||
else:
|
||||
self.remote_addr = '0.0.0.0'
|
||||
|
||||
def exp_backoff_keys(self):
|
||||
return self.cleaned_data['username'], self.remote_addr
|
||||
|
||||
def clean(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
keys = None
|
||||
if username and password:
|
||||
keys = self.exp_backoff_keys()
|
||||
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 = msg % int(math.ceil(seconds_to_wait))
|
||||
msg = html.mark_safe(msg)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
try:
|
||||
self.clean_authenticate()
|
||||
except Exception:
|
||||
if keys:
|
||||
self.exponential_backoff.failure(*keys)
|
||||
raise
|
||||
else:
|
||||
if keys:
|
||||
self.exponential_backoff.success(*keys)
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_authenticate(self):
|
||||
# copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
ou = self.cleaned_data.get('ou')
|
||||
|
||||
if username is not None and password:
|
||||
self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login',
|
||||
params={'username': self.username_field.verbose_name},
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(self.user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = super(AuthenticationForm, self).media
|
||||
media.add_js(['authentic2/js/js_seconds_until.js'])
|
||||
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
||||
media.add_js(['authentic2/js/ou_selector.js'])
|
||||
return media
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import warnings
|
||||
import io
|
||||
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.auth import forms as auth_forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import Form
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .. import models, hooks, app_settings, utils
|
||||
from ..backends import get_user_queryset
|
||||
from .fields import PasswordField, NewPasswordField, CheckPasswordField
|
||||
from .utils import NextUrlFormMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordResetForm(forms.Form):
|
||||
next_url = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
email = forms.EmailField(
|
||||
label=_("Email"), max_length=254)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Generates a one-use only link for resetting password and sends to the
|
||||
user.
|
||||
"""
|
||||
email = self.cleaned_data["email"].strip()
|
||||
users = get_user_queryset()
|
||||
active_users = users.filter(email__iexact=email, is_active=True)
|
||||
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)
|
||||
utils.send_password_reset_mail(
|
||||
user,
|
||||
set_random_password=set_random_password,
|
||||
next_url=self.cleaned_data.get('next_url'))
|
||||
if not active_users:
|
||||
logger.info(u'password reset requests for "%s", no user found')
|
||||
hooks.call_hooks('event', name='password-reset', email=email, users=active_users)
|
||||
|
||||
|
||||
class PasswordResetMixin(Form):
|
||||
'''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)
|
||||
if commit:
|
||||
models.PasswordReset.objects.filter(user=self.user).delete()
|
||||
else:
|
||||
old_save = self.user.save
|
||||
|
||||
def save(*args, **kwargs):
|
||||
ret = old_save(*args, **kwargs)
|
||||
models.PasswordReset.objects.filter(user=self.user).delete()
|
||||
return ret
|
||||
self.user.save = save
|
||||
return ret
|
||||
|
||||
|
||||
class NotifyOfPasswordChange(object):
|
||||
def save(self, commit=True):
|
||||
user = super(NotifyOfPasswordChange, self).save(commit=commit)
|
||||
if user.email:
|
||||
ctx = {
|
||||
'user': user,
|
||||
'password': self.cleaned_data['new_password1'],
|
||||
}
|
||||
utils.send_templated_mail(user, "authentic2/password_change", ctx)
|
||||
return user
|
||||
|
||||
|
||||
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm):
|
||||
new_password1 = NewPasswordField(label=_("New password"))
|
||||
new_password2 = CheckPasswordField(label=_("New password confirmation"))
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
if new_password1 and self.user.check_password(new_password1):
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
return new_password1
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
old_password = self.cleaned_data.get('old_password')
|
||||
if new_password1 and new_password1 == old_password:
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
return new_password1
|
||||
|
||||
# make old_password the first field
|
||||
new_base_fields = OrderedDict()
|
||||
|
||||
for k in ['old_password', 'new_password1', 'new_password2']:
|
||||
new_base_fields[k] = PasswordChangeForm.base_fields[k]
|
||||
|
||||
for k in PasswordChangeForm.base_fields:
|
||||
if k not in ['old_password', 'new_password1', 'new_password2']:
|
||||
new_base_fields[k] = PasswordChangeForm.base_fields[k]
|
||||
|
||||
PasswordChangeForm.base_fields = new_base_fields
|
|
@ -0,0 +1,168 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from django.forms.models import modelform_factory as dj_modelform_factory
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
|
||||
from ..custom_user.models import User
|
||||
from .. import app_settings, models
|
||||
from .utils import NextUrlFormMixin
|
||||
|
||||
|
||||
class DeleteAccountForm(forms.Form):
|
||||
password = forms.CharField(widget=forms.PasswordInput, label=_("Password"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
super(DeleteAccountForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get('password')
|
||||
if password and not self.user.check_password(password):
|
||||
raise forms.ValidationError(ugettext('Password is invalid'))
|
||||
return password
|
||||
|
||||
|
||||
class EmailChangeFormNoPassword(forms.Form):
|
||||
email = forms.EmailField(label=_('New email'))
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class EmailChangeForm(EmailChangeFormNoPassword):
|
||||
password = forms.CharField(label=_("Password"),
|
||||
widget=forms.PasswordInput)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
if email == self.user.email:
|
||||
raise forms.ValidationError(_('This is already your email address.'))
|
||||
return email
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data["password"]
|
||||
if not self.user.check_password(password):
|
||||
raise forms.ValidationError(
|
||||
_('Incorrect password.'),
|
||||
code='password_incorrect',
|
||||
)
|
||||
return password
|
||||
|
||||
|
||||
class BaseUserForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
'duplicate_username': _("A user with that username already exists."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from authentic2 import models
|
||||
|
||||
self.attributes = models.Attribute.objects.all()
|
||||
initial = kwargs.setdefault('initial', {})
|
||||
if kwargs.get('instance'):
|
||||
instance = kwargs['instance']
|
||||
for av in models.AttributeValue.objects.with_owner(instance):
|
||||
if av.attribute.name in self.declared_fields:
|
||||
if av.verified:
|
||||
self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly'
|
||||
initial[av.attribute.name] = av.to_python()
|
||||
super(BaseUserForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
from authentic2 import models
|
||||
|
||||
# make sure verified fields are not modified
|
||||
for av in models.AttributeValue.objects.with_owner(
|
||||
self.instance).filter(verified=True):
|
||||
self.cleaned_data[av.attribute.name] = av.to_python()
|
||||
super(BaseUserForm, self).clean()
|
||||
|
||||
def save_attributes(self):
|
||||
# only save non verified attributes here
|
||||
verified_attributes = set(
|
||||
self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True)
|
||||
)
|
||||
for attribute in self.attributes:
|
||||
name = attribute.name
|
||||
if name in self.fields and name not in verified_attributes:
|
||||
value = self.cleaned_data[name]
|
||||
setattr(self.instance.attributes, name, value)
|
||||
|
||||
def save(self, commit=True):
|
||||
result = super(BaseUserForm, self).save(commit=commit)
|
||||
if commit:
|
||||
self.save_attributes()
|
||||
else:
|
||||
old = self.save_m2m
|
||||
|
||||
def save_m2m(*args, **kwargs):
|
||||
old(*args, **kwargs)
|
||||
self.save_attributes()
|
||||
self.save_m2m = save_m2m
|
||||
return result
|
||||
|
||||
|
||||
class EditProfileForm(NextUrlFormMixin, BaseUserForm):
|
||||
pass
|
||||
|
||||
|
||||
def modelform_factory(model, **kwargs):
|
||||
'''Build a modelform for the given model,
|
||||
|
||||
For the user model also add attribute based fields.
|
||||
'''
|
||||
|
||||
form = kwargs.pop('form', None)
|
||||
fields = kwargs.get('fields') or []
|
||||
required = list(kwargs.pop('required', []) or [])
|
||||
d = {}
|
||||
# KV attributes are only supported for the user model currently
|
||||
modelform = None
|
||||
if issubclass(model, User):
|
||||
if not form:
|
||||
form = BaseUserForm
|
||||
attributes = models.Attribute.objects.all()
|
||||
for attribute in attributes:
|
||||
if attribute.name not in fields:
|
||||
continue
|
||||
d[attribute.name] = attribute.get_form_field()
|
||||
for field in app_settings.A2_REQUIRED_FIELDS:
|
||||
if field not in required:
|
||||
required.append(field)
|
||||
if not form or not hasattr(form, 'Meta'):
|
||||
meta_d = {'model': model, 'fields': '__all__'}
|
||||
meta = type('Meta', (), meta_d)
|
||||
d['Meta'] = meta
|
||||
if not form: # fallback
|
||||
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)
|
||||
|
||||
|
|
@ -1,28 +1,35 @@
|
|||
import re
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
import re
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
from django.forms import ModelForm, Form, CharField, PasswordInput, EmailField
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.forms.utils import ErrorList
|
||||
from django.forms import Form, EmailField
|
||||
|
||||
from django.contrib.auth.models import BaseUserManager, Group
|
||||
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME
|
||||
from django.core.mail import send_mail
|
||||
from django.core import signing
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
from authentic2.forms.fields import PasswordField, NewPasswordField, CheckPasswordField
|
||||
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks
|
||||
from authentic2.forms.fields import NewPasswordField, CheckPasswordField
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit
|
||||
|
||||
User = compat.get_user_model()
|
||||
from .. import app_settings, models
|
||||
from . import profile as profile_forms
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class RegistrationForm(Form):
|
||||
|
@ -53,7 +60,7 @@ class RegistrationForm(Form):
|
|||
return email
|
||||
|
||||
|
||||
class RegistrationCompletionFormNoPassword(forms.BaseUserForm):
|
||||
class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm):
|
||||
error_css_class = 'form-field-error'
|
||||
required_css_class = 'form-field-required'
|
||||
|
||||
|
@ -67,7 +74,6 @@ class RegistrationCompletionFormNoPassword(forms.BaseUserForm):
|
|||
ou = OrganizationalUnit.objects.get(pk=self.data['ou'])
|
||||
username_is_unique |= ou.username_is_unique
|
||||
if username_is_unique:
|
||||
User = get_user_model()
|
||||
exist = False
|
||||
try:
|
||||
User.objects.get(username=username)
|
||||
|
@ -86,7 +92,6 @@ class RegistrationCompletionFormNoPassword(forms.BaseUserForm):
|
|||
if self.cleaned_data.get('email'):
|
||||
email = self.cleaned_data['email']
|
||||
if app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE:
|
||||
User = get_user_model()
|
||||
exist = False
|
||||
try:
|
||||
User.objects.get(email__iexact=email)
|
||||
|
@ -130,80 +135,3 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
raise ValidationError(_("The two password fields didn't match."))
|
||||
self.instance.set_password(self.cleaned_data['password1'])
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class PasswordResetMixin(Form):
|
||||
'''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)
|
||||
if commit:
|
||||
models.PasswordReset.objects.filter(user=self.user).delete()
|
||||
else:
|
||||
old_save = self.user.save
|
||||
def save(*args, **kwargs):
|
||||
ret = old_save(*args, **kwargs)
|
||||
models.PasswordReset.objects.filter(user=self.user).delete()
|
||||
return ret
|
||||
self.user.save = save
|
||||
return ret
|
||||
|
||||
|
||||
class NotifyOfPasswordChange(object):
|
||||
def save(self, commit=True):
|
||||
user = super(NotifyOfPasswordChange, self).save(commit=commit)
|
||||
if user.email:
|
||||
ctx = {
|
||||
'user': user,
|
||||
'password': self.cleaned_data['new_password1'],
|
||||
}
|
||||
utils.send_templated_mail(user, "authentic2/password_change", ctx)
|
||||
return user
|
||||
|
||||
|
||||
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm):
|
||||
new_password1 = NewPasswordField(label=_("New password"))
|
||||
new_password2 = CheckPasswordField(label=_("New password confirmation"))
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
if new_password1 and self.user.check_password(new_password1):
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
return new_password1
|
||||
|
||||
|
||||
class PasswordChangeForm(NotifyOfPasswordChange, forms.NextUrlFormMixin, PasswordResetMixin,
|
||||
auth_forms.PasswordChangeForm):
|
||||
old_password = PasswordField(label=_('Old password'))
|
||||
new_password1 = NewPasswordField(label=_('New password'))
|
||||
new_password2 = CheckPasswordField(label=_("New password confirmation"))
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
old_password = self.cleaned_data.get('old_password')
|
||||
if new_password1 and new_password1 == old_password:
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
return new_password1
|
||||
|
||||
# make old_password the first field
|
||||
PasswordChangeForm.base_fields = OrderedDict(
|
||||
[(k, PasswordChangeForm.base_fields[k])
|
||||
for k in ['old_password', 'new_password1', 'new_password2']] +
|
||||
[(k, PasswordChangeForm.base_fields[k])
|
||||
for k in PasswordChangeForm.base_fields if k not in ['old_password', 'new_password1',
|
||||
'new_password2']]
|
||||
)
|
||||
|
||||
class DeleteAccountForm(Form):
|
||||
password = CharField(widget=PasswordInput, label=_("Password"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
super(DeleteAccountForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get('password')
|
||||
if password and not self.user.check_password(password):
|
||||
raise ValidationError(ugettext('Password is invalid'))
|
||||
return password
|
|
@ -0,0 +1,33 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
|
||||
from ..middleware import StoreRequestMiddleware
|
||||
|
||||
|
||||
class NextUrlFormMixin(forms.Form):
|
||||
next_url = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
next_url = kwargs.pop('next_url', None)
|
||||
request = StoreRequestMiddleware.get_request()
|
||||
if not next_url and request:
|
||||
next_url = request.GET.get(REDIRECT_FIELD_NAME)
|
||||
super(NextUrlFormMixin, self).__init__(*args, **kwargs)
|
||||
if next_url:
|
||||
self.fields['next_url'].initial = next_url
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Bootstrap django-datetime-widget is a simple and clean widget for DateField,
|
||||
# Timefiled and DateTimeField in Django framework. It is based on Bootstrap
|
||||
# datetime picker, supports Bootstrap 2
|
||||
|
@ -12,8 +28,7 @@ import re
|
|||
import uuid
|
||||
|
||||
import django
|
||||
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \
|
||||
ClearableFileInput
|
||||
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, ClearableFileInput
|
||||
from django.forms.widgets import PasswordInput as BasePasswordInput
|
||||
from django.utils.formats import get_language, get_format
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -95,8 +110,7 @@ class PickerWidgetMixin(object):
|
|||
date_format = self.options['format']
|
||||
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub(
|
||||
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()],
|
||||
date_format
|
||||
)
|
||||
date_format)
|
||||
|
||||
super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
|
||||
|
||||
|
@ -112,7 +126,7 @@ class PickerWidgetMixin(object):
|
|||
final_attrs['class'] = "controls input-append date"
|
||||
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs)
|
||||
|
||||
#if not set, autoclose have to be true.
|
||||
# if not set, autoclose have to be true.
|
||||
self.options.setdefault('autoclose', True)
|
||||
|
||||
# Build javascript options out of python dictionary
|
||||
|
@ -130,14 +144,12 @@ class PickerWidgetMixin(object):
|
|||
help_text = u'%s %s' % (_('Format:'), self.options['format'])
|
||||
|
||||
return mark_safe(BOOTSTRAP_INPUT_TEMPLATE % dict(
|
||||
id=id,
|
||||
rendered_widget=rendered_widget,
|
||||
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
|
||||
glyphicon=self.glyphicon,
|
||||
options=js_options,
|
||||
help_text=help_text,
|
||||
)
|
||||
)
|
||||
id=id,
|
||||
rendered_widget=rendered_widget,
|
||||
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
|
||||
glyphicon=self.glyphicon,
|
||||
options=js_options,
|
||||
help_text=help_text))
|
||||
|
||||
|
||||
class DateTimeWidget(PickerWidgetMixin, DateTimeInput):
|
||||
|
@ -253,8 +265,8 @@ class CheckPasswordInput(PasswordInput):
|
|||
class ProfileImageInput(ClearableFileInput):
|
||||
if django.VERSION < (1, 9):
|
||||
template_with_initial = (
|
||||
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> '
|
||||
'%(clear_template)s<br />%(input_text)s: %(input)s'
|
||||
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> '
|
||||
'%(clear_template)s<br />%(input_text)s: %(input)s'
|
||||
)
|
||||
else:
|
||||
template_name = "authentic2/profile_image_input.html"
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import base64
|
||||
|
@ -66,7 +82,7 @@ class Drupal7PasswordHasher(hashers.BasePasswordHasher):
|
|||
assert salt and '$' not in salt
|
||||
h = salt
|
||||
password = force_bytes(password)
|
||||
for i in xrange(iterations+1):
|
||||
for i in range(iterations + 1):
|
||||
h = self.digest(h + password).digest()
|
||||
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, self.b64encode(h)[:43])
|
||||
|
||||
|
@ -117,11 +133,26 @@ class CommonPasswordHasher(hashers.BasePasswordHasher):
|
|||
|
||||
|
||||
OPENLDAP_ALGO_MAPPING = {
|
||||
# hasher? salt offset? hex encode?
|
||||
'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
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
|
@ -37,7 +53,7 @@ def call_hooks(hook_name, *args, **kwargs):
|
|||
for hook in hooks:
|
||||
try:
|
||||
yield hook(*args, **kwargs)
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception(u'exception while calling hook %s', hook)
|
||||
|
||||
|
||||
|
@ -50,5 +66,5 @@ def call_hooks_first_result(hook_name, *args, **kwargs):
|
|||
result = hook(*args, **kwargs)
|
||||
if result is not None:
|
||||
return result
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception(u'exception while calling hook %s', hook)
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import requests
|
||||
|
||||
from authentic2 import app_settings
|
||||
|
||||
|
||||
def get_url(url):
|
||||
'''Does a simple GET on an URL, check the certificate'''
|
||||
verify = app_settings.A2_VERIFY_SSL
|
||||
|
|
|
@ -1,83 +1,43 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
|
||||
from authentic2.saml.models import LibertyProvider
|
||||
|
||||
|
||||
@login_required
|
||||
def consent_federation(request, nonce = '', next = None, provider_id = None):
|
||||
def consent_federation(request, nonce='', provider_id=None):
|
||||
'''On a GET produce a form asking for consentment,
|
||||
On a POST handle the form and redirect to next'''
|
||||
if request.method == "GET":
|
||||
return render(request, 'interaction/consent_federation.html',
|
||||
{'provider_id': request.GET.get('provider_id', ''),
|
||||
'nonce': request.GET.get('nonce', ''),
|
||||
'next': request.GET.get('next', '')})
|
||||
return render(
|
||||
request, 'interaction/consent_federation.html',
|
||||
{
|
||||
'provider_id': request.GET.get('provider_id', ''),
|
||||
'nonce': request.GET.get('nonce', ''),
|
||||
'next': request.GET.get('next', '')
|
||||
})
|
||||
else:
|
||||
next = '/'
|
||||
next_url = '/'
|
||||
if 'next' in request.POST:
|
||||
next = request.POST['next']
|
||||
next_url = request.POST['next']
|
||||
if 'accept' in request.POST:
|
||||
next = next + '&consent_answer=accepted'
|
||||
return HttpResponseRedirect(next)
|
||||
next_url = next_url + '&consent_answer=accepted'
|
||||
return HttpResponseRedirect(next_url)
|
||||
else:
|
||||
next = next + '&consent_answer=refused'
|
||||
next_url = next_url + '&consent_answer=refused'
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
@login_required
|
||||
def consent_attributes(request, nonce = '', next = None, provider_id = None):
|
||||
'''On a GET produce a form asking for consentment,
|
||||
On a POST handle the form and redirect to next'''
|
||||
provider = None
|
||||
try:
|
||||
provider = LibertyProvider.objects.get(entity_id=request.GET.get('provider_id', ''))
|
||||
except:
|
||||
pass
|
||||
next = '/'
|
||||
|
||||
if request.method == "GET":
|
||||
attributes = []
|
||||
next = request.GET.get('next', '')
|
||||
if 'attributes_to_send' in request.session:
|
||||
i = 0
|
||||
for key, values in request.session['attributes_to_send'].items():
|
||||
name = None
|
||||
if type(key) is tuple and len(key) == 3:
|
||||
_, _, name = key
|
||||
elif type(key) is tuple and len(key) == 2:
|
||||
name, _, = key
|
||||
else:
|
||||
name = key
|
||||
if name and values:
|
||||
attributes.append((i, name, values))
|
||||
i = i + 1
|
||||
name = request.GET.get('provider_id', '')
|
||||
if provider:
|
||||
name = provider.name or name
|
||||
return render(request, 'interaction/consent_attributes.html',
|
||||
{'provider_id': name,
|
||||
'attributes': attributes,
|
||||
'allow_selection': request.session['allow_attributes_selection'],
|
||||
'nonce': request.GET.get('nonce', ''),
|
||||
'next': next})
|
||||
|
||||
elif request.method == "POST":
|
||||
if request.session['allow_attributes_selection']:
|
||||
vals = \
|
||||
[int(value) for key, value in request.POST.items() \
|
||||
if 'attribute_nb' in key]
|
||||
attributes_to_send = dict()
|
||||
i = 0
|
||||
for k, v in request.session['attributes_to_send'].items():
|
||||
if i in vals:
|
||||
attributes_to_send[k] = v
|
||||
i = i + 1
|
||||
request.session['attributes_to_send'] = attributes_to_send
|
||||
if 'next' in request.POST:
|
||||
next = request.POST['next']
|
||||
if 'accept' in request.POST:
|
||||
next = next + '&consent_attribute_answer=accepted'
|
||||
else:
|
||||
next = next + '&consent_attribute_answer=refused'
|
||||
return HttpResponseRedirect(next)
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import warnings
|
||||
|
||||
from authentic2.idp.management.commands import cleanupauthentic
|
||||
|
||||
|
||||
class Command(cleanupauthentic.Command):
|
||||
def handle_noargs(self, **options):
|
||||
warnings.warn(
|
||||
"The `cleanup` command has been deprecated in favor of `cleanupauthentic`.",
|
||||
PendingDeprecationWarning)
|
||||
super(Command, self).handle_noargs(**options)
|
|
@ -1,22 +1,39 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Clean expired models of authentic2.'
|
||||
|
||||
def handle(self, **options):
|
||||
log = logging.getLogger(__name__)
|
||||
for app in apps.get_app_configs():
|
||||
for model in app.get_models():
|
||||
# only models from authentic2
|
||||
if model.__module__.startswith('authentic2'):
|
||||
try:
|
||||
self.cleanup_model(model)
|
||||
except:
|
||||
log.exception('cleanup of model %s failed', model)
|
||||
except Exception:
|
||||
logger.exception('cleanup of model %s failed', model)
|
||||
|
||||
def cleanup_model(self, model):
|
||||
manager = getattr(model, 'objects', None)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
class DebugMiddleware:
|
||||
def process_exception(self, request, exception):
|
||||
if getattr(settings, 'DEBUG', False):
|
||||
traceback.print_exc()
|
|
@ -1,5 +1,20 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.checks import register, Warning, Tags
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
@ -44,9 +59,10 @@ def check_authentic2_config(app_configs, **kwargs):
|
|||
from . import app_settings
|
||||
errors = []
|
||||
|
||||
if not settings.DEBUG and app_settings.ENABLE and \
|
||||
(app_settings.is_default('SIGNATURE_PUBLIC_KEY') or
|
||||
app_settings.is_default('SIGNATURE_PRIVATE_KEY')):
|
||||
if (not settings.DEBUG
|
||||
and app_settings.ENABLE
|
||||
and (app_settings.is_default('SIGNATURE_PUBLIC_KEY')
|
||||
or app_settings.is_default('SIGNATURE_PRIVATE_KEY'))):
|
||||
errors.append(
|
||||
Warning(
|
||||
'You should not use default SAML keys in production',
|
||||
|
@ -57,5 +73,4 @@ def check_authentic2_config(app_configs, **kwargs):
|
|||
)
|
||||
return errors
|
||||
|
||||
check_authentic2_config = register(Tags.security,
|
||||
deploy=True)(check_authentic2_config)
|
||||
check_authentic2_config = register(Tags.security, deploy=True)(check_authentic2_config)
|
||||
|
|
|
@ -1,10 +1,29 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class AppSettings(object):
|
||||
__DEFAULTS = dict(
|
||||
ENABLE=False,
|
||||
METADATA_OPTIONS={},
|
||||
SECONDS_TOLERANCE=60,
|
||||
AUTHN_CONTEXT_FROM_SESSION=True,
|
||||
SIGNATURE_PUBLIC_KEY = '''-----BEGIN CERTIFICATE-----
|
||||
ENABLE=False,
|
||||
METADATA_OPTIONS={},
|
||||
SECONDS_TOLERANCE=60,
|
||||
AUTHN_CONTEXT_FROM_SESSION=True,
|
||||
SIGNATURE_PUBLIC_KEY='''-----BEGIN CERTIFICATE-----
|
||||
MIIDIzCCAgugAwIBAgIJANUBoick1pDpMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
|
||||
BAoTCkVudHJvdXZlcnQwHhcNMTAxMjE0MTUzMzAyWhcNMTEwMTEzMTUzMzAyWjAV
|
||||
MRMwEQYDVQQKEwpFbnRyb3V2ZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
|
@ -23,7 +42,7 @@ lG6l41SXp6YgIb2ToT+rOKdIGIQuGDlzeR88fDxWEU0vEujZv/v1PE1YOV0xKjTT
|
|||
JumlBc6IViKhJeo1wiBBrVRIIkKKevHKQzteK8pWm9CYWculxT26TZ4VWzGbo06j
|
||||
o2zbumirrLLqnt1gmBDvDvlOwC/zAAyL4chbz66eQHTiIYZZvYgy
|
||||
-----END CERTIFICATE-----''',
|
||||
SIGNATURE_PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
|
||||
SIGNATURE_PRIVATE_KEY='''-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAvxFkfPdndlGgQPDZgFGXbrNAc/79PULZBuNdWFHDD9P5hNhZ
|
||||
n9Kqm4Cp06Pe/A6u+g5wLnYvbZQcFCgfQAEzziJtb3J55OOlB7iMEI/T2AX2WzrU
|
||||
H8QT8NGhABONKU2Gg4XiyeXNhH5R7zdHlUwcWq3ZwNbtbY0TVc+n665EbrfV/59x
|
||||
|
@ -50,8 +69,8 @@ gmsgaiMCgYB/nrTk89Fp7050VKCNnIt1mHAcO9cBwDV8qrJ5O3rIVmrg1T6vn0aY
|
|||
wRiVcNacaP+BivkrMjr4BlsUM6yH4MOBsNhLURiiCL+tLJV7U0DWlCse/doWij4U
|
||||
TKX6tp6oI+7MIJE6ySZ0cBqOiydAkBePZhu57j6ToBkTa0dbHjn1WA==
|
||||
-----END RSA PRIVATE KEY-----''',
|
||||
ADD_CERTIFICATE_TO_KEY_INFO=True,
|
||||
SIGNATURE_METHOD='RSA-SHA256',
|
||||
ADD_CERTIFICATE_TO_KEY_INFO=True,
|
||||
SIGNATURE_METHOD='RSA-SHA256',
|
||||
)
|
||||
|
||||
def __init__(self, prefix):
|
||||
|
@ -67,26 +86,26 @@ TKX6tp6oI+7MIJE6ySZ0cBqOiydAkBePZhu57j6ToBkTa0dbHjn1WA==
|
|||
@property
|
||||
def ENABLE(self):
|
||||
return self._setting_with_prefix('ENABLE',
|
||||
self._setting('IDP_SAML2',
|
||||
self.__DEFAULTS['ENABLE']))
|
||||
self._setting('IDP_SAML2',
|
||||
self.__DEFAULTS['ENABLE']))
|
||||
|
||||
@property
|
||||
def SIGNATURE_PUBLIC_KEY(self):
|
||||
return self._setting_with_prefix('SIGNATURE_PUBLIC_KEY',
|
||||
self._setting('SAML_SIGNATURE_PUBLIC_KEY',
|
||||
self.__DEFAULTS['SIGNATURE_PUBLIC_KEY']))
|
||||
self._setting('SAML_SIGNATURE_PUBLIC_KEY',
|
||||
self.__DEFAULTS['SIGNATURE_PUBLIC_KEY']))
|
||||
|
||||
@property
|
||||
def SIGNATURE_PRIVATE_KEY(self):
|
||||
return self._setting_with_prefix('SIGNATURE_PRIVATE_KEY',
|
||||
self._setting('SAML_SIGNATURE_PRIVATE_KEY',
|
||||
self.__DEFAULTS['SIGNATURE_PRIVATE_KEY']))
|
||||
self._setting('SAML_SIGNATURE_PRIVATE_KEY',
|
||||
self.__DEFAULTS['SIGNATURE_PRIVATE_KEY']))
|
||||
|
||||
@property
|
||||
def AUTHN_CONTEXT_FROM_SESSION(self):
|
||||
return self._setting_with_prefix('AUTHN_CONTEXT_FROM_SESSION',
|
||||
self._setting('IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION',
|
||||
self.__DEFAULTS['AUTHN_CONTEXT_FROM_SESSION']))
|
||||
self._setting('IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION',
|
||||
self.__DEFAULTS['AUTHN_CONTEXT_FROM_SESSION']))
|
||||
|
||||
def is_default(self, name):
|
||||
return getattr(self, name) == self.__DEFAULTS[name]
|
||||
|
@ -96,10 +115,8 @@ TKX6tp6oI+7MIJE6ySZ0cBqOiydAkBePZhu57j6ToBkTa0dbHjn1WA==
|
|||
raise AttributeError(name)
|
||||
return self._setting_with_prefix(name, self.__DEFAULTS[name])
|
||||
|
||||
|
||||
# Ugly? Guido recommends this himself ...
|
||||
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||
import sys
|
||||
app_settings = AppSettings('A2_IDP_SAML2_')
|
||||
app_settings.__name__ = __name__
|
||||
sys.modules[__name__] = app_settings
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, SESSION_KEY
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.utils.http import urlencode
|
||||
from importlib import import_module
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
def redirect_to_login(next, login_url=None,
|
||||
redirect_field_name=REDIRECT_FIELD_NAME, other_keys = {}):
|
||||
|
||||
def redirect_to_login(next_url, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME, other_keys={}):
|
||||
"Redirects the user to the login page, passing the given 'next' page"
|
||||
if not login_url:
|
||||
login_url = settings.LOGIN_URL
|
||||
data = { redirect_field_name: next }
|
||||
data = {redirect_field_name: next_url}
|
||||
for k, v in other_keys.items():
|
||||
data[k] = v
|
||||
return HttpResponseRedirect('%s?%s' % (login_url, urlencode(data)))
|
||||
|
||||
|
||||
def kill_django_sessions(session_key):
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
try:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import DeleteView, View
|
||||
|
@ -8,9 +24,11 @@ from django.contrib import messages
|
|||
|
||||
from authentic2.saml.models import LibertyFederation
|
||||
|
||||
|
||||
class FederationCreateView(View):
|
||||
pass
|
||||
|
||||
|
||||
class FederationDeleteView(DeleteView):
|
||||
model = LibertyFederation
|
||||
|
||||
|
@ -28,8 +46,7 @@ class FederationDeleteView(DeleteView):
|
|||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.POST.get(REDIRECT_FIELD_NAME,
|
||||
reverse('auth_homepage'))
|
||||
return self.request.POST.get(REDIRECT_FIELD_NAME, reverse('auth_homepage'))
|
||||
|
||||
|
||||
delete_federation = FederationDeleteView.as_view()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue