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:
Benjamin Dauvergne 2019-05-08 10:56:49 +02:00
parent 7a0d5719d8
commit 9fbbf0519a
262 changed files with 6221 additions and 4039 deletions

View File

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

View File

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

View File

@ -1,2 +0,0 @@
authentic2-plugin-template is entirely under the copyright of Entr'ouvert and
distributed under the license AGPLv3 or later.

View File

@ -1,3 +0,0 @@
include COPYING
recursive-include src/authentic2_plugin_template/templates *.html
recursive-include src/authentic2_plugin_template/static *.js *.css *.png

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
from django.contrib import admin
from . import models
# registrer your admin editable models here using admin.register

View File

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

View File

@ -1,3 +0,0 @@
from django import forms

View File

@ -1,4 +0,0 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
# put your models here

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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