0001-spring-cleaning-32934.patch
MANIFEST.in | ||
---|---|---|
10 | 10 |
recursive-include src/authentic2/manager/static *.css *.js *.png |
11 | 11 | |
12 | 12 |
# templates |
13 |
recursive-include src/authentic2/saml/templates *.html *.txt *.xml |
|
14 | 13 |
recursive-include src/authentic2/templates *.html *.txt *.xml |
14 |
recursive-include src/authentic2/manager/templates *.html *.txt |
|
15 |
recursive-include src/authentic2/saml/templates *.html *.txt *.xml |
|
15 | 16 |
recursive-include src/authentic2/idp/templates *.html *.txt *.xml |
16 | 17 |
recursive-include src/authentic2_idp_cas/templates *.html *.txt *.xml |
17 | 18 |
recursive-include src/authentic2/idp/saml/templates *.html *.txt *.xml |
18 | 19 |
recursive-include src/authentic2/auth2_auth/auth2_ssl/templates *.html *.txt *.xml |
19 | 20 |
recursive-include src/authentic2/auth2_auth/templates *.html *.txt *.xml |
20 | 21 |
recursive-include src/authentic2/auth2_auth/auth2_oath/templates *.html *.txt *.xml |
21 |
recursive-include src/authentic2/manager/templates *.html |
|
22 | 22 |
recursive-include src/authentic2_auth_saml/templates/authentic2_auth_saml *.html |
23 | 23 |
recursive-include src/authentic2_auth_oidc/templates/authentic2_auth_oidc *.html |
24 | 24 |
recursive-include src/authentic2_idp_oidc/templates/authentic2_idp_oidc *.html |
25 | 25 | |
26 |
recursive-include src/authentic2/vendor/totp_js/js *.js |
|
27 | 26 |
recursive-include src/authentic2/saml/fixtures *.json |
28 | 27 |
recursive-include src/authentic2/locale *.po *.mo |
29 | 28 |
recursive-include src/authentic2/saml/locale *.po *.mo |
... | ... | |
42 | 41 |
recursive-include src/authentic2_auth_oidc/locale *.po *.mo |
43 | 42 |
recursive-include src/authentic2_idp_oidc/locale *.po *.mo |
44 | 43 | |
45 |
recursive-include src/authentic2 README xrds.xml *.txt yadis.xrdf
|
|
44 |
recursive-include src/authentic2 README |
|
46 | 45 |
recursive-include src/authentic2_provisionning_ldap/tests *.ldif |
47 | 46 |
recursive-include src/authentic2_provisionning_ldap/tests *.ldif |
48 | 47 | |
49 |
recursive-include samples * |
|
50 | ||
51 | 48 |
include doc/*.rst |
52 | 49 |
include doc/pictures/* |
53 |
include COPYING NEWS README.rst AUTHORS.txt |
|
54 |
include src/authentic2/vendor/oath/TODO |
|
55 |
include src/authentic2/vendor/totp_js/README.rst |
|
56 |
include diagnose.py |
|
57 |
include ez_setup.py |
|
50 |
include COPYING NEWS README AUTHORS.txt |
|
58 | 51 |
include src/authentic2/auth2_auth/auth2_ssl/authentic_ssl.vhost |
59 |
include requirements.txt |
|
60 |
include test_settings |
|
61 | 52 |
include getlasso.sh |
62 | 53 |
include getlasso3.sh |
63 | 54 |
include src/authentic2/nonce/README.rst |
64 |
include doc/conf.py doc/Makefile doc/README.rst.bak
|
|
55 |
include doc/conf.py doc/Makefile |
|
65 | 56 |
include local_settings.py.example |
66 | 57 |
include MANIFEST.in |
67 | 58 |
include VERSION |
ez_setup.py | ||
---|---|---|
1 |
#!python |
|
2 |
"""Bootstrap distribute installation |
|
3 | ||
4 |
If you want to use setuptools in your package's setup.py, just include this |
|
5 |
file in the same directory with it, and add this to the top of your setup.py:: |
|
6 | ||
7 |
from distribute_setup import use_setuptools |
|
8 |
use_setuptools() |
|
9 | ||
10 |
If you want to require a specific version of setuptools, set a download |
|
11 |
mirror, or use an alternate download directory, you can do so by supplying |
|
12 |
the appropriate options to ``use_setuptools()``. |
|
13 | ||
14 |
This file can also be run as a script to install or upgrade setuptools. |
|
15 |
""" |
|
16 |
import os |
|
17 |
import sys |
|
18 |
import time |
|
19 |
import fnmatch |
|
20 |
import tempfile |
|
21 |
import tarfile |
|
22 |
from distutils import log |
|
23 | ||
24 |
try: |
|
25 |
from site import USER_SITE |
|
26 |
except ImportError: |
|
27 |
USER_SITE = None |
|
28 | ||
29 |
try: |
|
30 |
import subprocess |
|
31 | ||
32 |
def _python_cmd(*args): |
|
33 |
args = (sys.executable,) + args |
|
34 |
return subprocess.call(args) == 0 |
|
35 | ||
36 |
except ImportError: |
|
37 |
# will be used for python 2.3 |
|
38 |
def _python_cmd(*args): |
|
39 |
args = (sys.executable,) + args |
|
40 |
# quoting arguments if windows |
|
41 |
if sys.platform == 'win32': |
|
42 |
def quote(arg): |
|
43 |
if ' ' in arg: |
|
44 |
return '"%s"' % arg |
|
45 |
return arg |
|
46 |
args = [quote(arg) for arg in args] |
|
47 |
return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 |
|
48 | ||
49 |
DEFAULT_VERSION = "0.6.14" |
|
50 |
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" |
|
51 |
SETUPTOOLS_FAKED_VERSION = "0.6c11" |
|
52 | ||
53 |
SETUPTOOLS_PKG_INFO = """\ |
|
54 |
Metadata-Version: 1.0 |
|
55 |
Name: setuptools |
|
56 |
Version: %s |
|
57 |
Summary: xxxx |
|
58 |
Home-page: xxx |
|
59 |
Author: xxx |
|
60 |
Author-email: xxx |
|
61 |
License: xxx |
|
62 |
Description: xxx |
|
63 |
""" % SETUPTOOLS_FAKED_VERSION |
|
64 | ||
65 | ||
66 |
def _install(tarball): |
|
67 |
# extracting the tarball |
|
68 |
tmpdir = tempfile.mkdtemp() |
|
69 |
log.warn('Extracting in %s', tmpdir) |
|
70 |
old_wd = os.getcwd() |
|
71 |
try: |
|
72 |
os.chdir(tmpdir) |
|
73 |
tar = tarfile.open(tarball) |
|
74 |
_extractall(tar) |
|
75 |
tar.close() |
|
76 | ||
77 |
# going in the directory |
|
78 |
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) |
|
79 |
os.chdir(subdir) |
|
80 |
log.warn('Now working in %s', subdir) |
|
81 | ||
82 |
# installing |
|
83 |
log.warn('Installing Distribute') |
|
84 |
if not _python_cmd('setup.py', 'install'): |
|
85 |
log.warn('Something went wrong during the installation.') |
|
86 |
log.warn('See the error message above.') |
|
87 |
finally: |
|
88 |
os.chdir(old_wd) |
|
89 | ||
90 | ||
91 |
def _build_egg(egg, tarball, to_dir): |
|
92 |
# extracting the tarball |
|
93 |
tmpdir = tempfile.mkdtemp() |
|
94 |
log.warn('Extracting in %s', tmpdir) |
|
95 |
old_wd = os.getcwd() |
|
96 |
try: |
|
97 |
os.chdir(tmpdir) |
|
98 |
tar = tarfile.open(tarball) |
|
99 |
_extractall(tar) |
|
100 |
tar.close() |
|
101 | ||
102 |
# going in the directory |
|
103 |
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) |
|
104 |
os.chdir(subdir) |
|
105 |
log.warn('Now working in %s', subdir) |
|
106 | ||
107 |
# building an egg |
|
108 |
log.warn('Building a Distribute egg in %s', to_dir) |
|
109 |
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) |
|
110 | ||
111 |
finally: |
|
112 |
os.chdir(old_wd) |
|
113 |
# returning the result |
|
114 |
log.warn(egg) |
|
115 |
if not os.path.exists(egg): |
|
116 |
raise IOError('Could not build the egg.') |
|
117 | ||
118 | ||
119 |
def _do_download(version, download_base, to_dir, download_delay): |
|
120 |
egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' |
|
121 |
% (version, sys.version_info[0], sys.version_info[1])) |
|
122 |
if not os.path.exists(egg): |
|
123 |
tarball = download_setuptools(version, download_base, |
|
124 |
to_dir, download_delay) |
|
125 |
_build_egg(egg, tarball, to_dir) |
|
126 |
sys.path.insert(0, egg) |
|
127 |
import setuptools |
|
128 |
setuptools.bootstrap_install_from = egg |
|
129 | ||
130 | ||
131 |
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, |
|
132 |
to_dir=os.curdir, download_delay=15, no_fake=True): |
|
133 |
# making sure we use the absolute path |
|
134 |
to_dir = os.path.abspath(to_dir) |
|
135 |
was_imported = 'pkg_resources' in sys.modules or \ |
|
136 |
'setuptools' in sys.modules |
|
137 |
try: |
|
138 |
try: |
|
139 |
import pkg_resources |
|
140 |
if not hasattr(pkg_resources, '_distribute'): |
|
141 |
if not no_fake: |
|
142 |
_fake_setuptools() |
|
143 |
raise ImportError |
|
144 |
except ImportError: |
|
145 |
return _do_download(version, download_base, to_dir, download_delay) |
|
146 |
try: |
|
147 |
pkg_resources.require("distribute>="+version) |
|
148 |
return |
|
149 |
except pkg_resources.VersionConflict: |
|
150 |
e = sys.exc_info()[1] |
|
151 |
if was_imported: |
|
152 |
sys.stderr.write( |
|
153 |
"The required version of distribute (>=%s) is not available,\n" |
|
154 |
"and can't be installed while this script is running. Please\n" |
|
155 |
"install a more recent version first, using\n" |
|
156 |
"'easy_install -U distribute'." |
|
157 |
"\n\n(Currently using %r)\n" % (version, e.args[0])) |
|
158 |
sys.exit(2) |
|
159 |
else: |
|
160 |
del pkg_resources, sys.modules['pkg_resources'] # reload ok |
|
161 |
return _do_download(version, download_base, to_dir, |
|
162 |
download_delay) |
|
163 |
except pkg_resources.DistributionNotFound: |
|
164 |
return _do_download(version, download_base, to_dir, |
|
165 |
download_delay) |
|
166 |
finally: |
|
167 |
if not no_fake: |
|
168 |
_create_fake_setuptools_pkg_info(to_dir) |
|
169 | ||
170 |
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, |
|
171 |
to_dir=os.curdir, delay=15): |
|
172 |
"""Download distribute from a specified location and return its filename |
|
173 | ||
174 |
`version` should be a valid distribute version number that is available |
|
175 |
as an egg for download under the `download_base` URL (which should end |
|
176 |
with a '/'). `to_dir` is the directory where the egg will be downloaded. |
|
177 |
`delay` is the number of seconds to pause before an actual download |
|
178 |
attempt. |
|
179 |
""" |
|
180 |
# making sure we use the absolute path |
|
181 |
to_dir = os.path.abspath(to_dir) |
|
182 |
try: |
|
183 |
from urllib.request import urlopen |
|
184 |
except ImportError: |
|
185 |
from urllib2 import urlopen |
|
186 |
tgz_name = "distribute-%s.tar.gz" % version |
|
187 |
url = download_base + tgz_name |
|
188 |
saveto = os.path.join(to_dir, tgz_name) |
|
189 |
src = dst = None |
|
190 |
if not os.path.exists(saveto): # Avoid repeated downloads |
|
191 |
try: |
|
192 |
log.warn("Downloading %s", url) |
|
193 |
src = urlopen(url) |
|
194 |
# Read/write all in one block, so we don't create a corrupt file |
|
195 |
# if the download is interrupted. |
|
196 |
data = src.read() |
|
197 |
dst = open(saveto, "wb") |
|
198 |
dst.write(data) |
|
199 |
finally: |
|
200 |
if src: |
|
201 |
src.close() |
|
202 |
if dst: |
|
203 |
dst.close() |
|
204 |
return os.path.realpath(saveto) |
|
205 | ||
206 |
def _no_sandbox(function): |
|
207 |
def __no_sandbox(*args, **kw): |
|
208 |
try: |
|
209 |
from setuptools.sandbox import DirectorySandbox |
|
210 |
if not hasattr(DirectorySandbox, '_old'): |
|
211 |
def violation(*args): |
|
212 |
pass |
|
213 |
DirectorySandbox._old = DirectorySandbox._violation |
|
214 |
DirectorySandbox._violation = violation |
|
215 |
patched = True |
|
216 |
else: |
|
217 |
patched = False |
|
218 |
except ImportError: |
|
219 |
patched = False |
|
220 | ||
221 |
try: |
|
222 |
return function(*args, **kw) |
|
223 |
finally: |
|
224 |
if patched: |
|
225 |
DirectorySandbox._violation = DirectorySandbox._old |
|
226 |
del DirectorySandbox._old |
|
227 | ||
228 |
return __no_sandbox |
|
229 | ||
230 |
def _patch_file(path, content): |
|
231 |
"""Will backup the file then patch it""" |
|
232 |
existing_content = open(path).read() |
|
233 |
if existing_content == content: |
|
234 |
# already patched |
|
235 |
log.warn('Already patched.') |
|
236 |
return False |
|
237 |
log.warn('Patching...') |
|
238 |
_rename_path(path) |
|
239 |
f = open(path, 'w') |
|
240 |
try: |
|
241 |
f.write(content) |
|
242 |
finally: |
|
243 |
f.close() |
|
244 |
return True |
|
245 | ||
246 |
_patch_file = _no_sandbox(_patch_file) |
|
247 | ||
248 |
def _same_content(path, content): |
|
249 |
return open(path).read() == content |
|
250 | ||
251 |
def _rename_path(path): |
|
252 |
new_name = path + '.OLD.%s' % time.time() |
|
253 |
log.warn('Renaming %s into %s', path, new_name) |
|
254 |
os.rename(path, new_name) |
|
255 |
return new_name |
|
256 | ||
257 |
def _remove_flat_installation(placeholder): |
|
258 |
if not os.path.isdir(placeholder): |
|
259 |
log.warn('Unkown installation at %s', placeholder) |
|
260 |
return False |
|
261 |
found = False |
|
262 |
for file in os.listdir(placeholder): |
|
263 |
if fnmatch.fnmatch(file, 'setuptools*.egg-info'): |
|
264 |
found = True |
|
265 |
break |
|
266 |
if not found: |
|
267 |
log.warn('Could not locate setuptools*.egg-info') |
|
268 |
return |
|
269 | ||
270 |
log.warn('Removing elements out of the way...') |
|
271 |
pkg_info = os.path.join(placeholder, file) |
|
272 |
if os.path.isdir(pkg_info): |
|
273 |
patched = _patch_egg_dir(pkg_info) |
|
274 |
else: |
|
275 |
patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) |
|
276 | ||
277 |
if not patched: |
|
278 |
log.warn('%s already patched.', pkg_info) |
|
279 |
return False |
|
280 |
# now let's move the files out of the way |
|
281 |
for element in ('setuptools', 'pkg_resources.py', 'site.py'): |
|
282 |
element = os.path.join(placeholder, element) |
|
283 |
if os.path.exists(element): |
|
284 |
_rename_path(element) |
|
285 |
else: |
|
286 |
log.warn('Could not find the %s element of the ' |
|
287 |
'Setuptools distribution', element) |
|
288 |
return True |
|
289 | ||
290 |
_remove_flat_installation = _no_sandbox(_remove_flat_installation) |
|
291 | ||
292 |
def _after_install(dist): |
|
293 |
log.warn('After install bootstrap.') |
|
294 |
placeholder = dist.get_command_obj('install').install_purelib |
|
295 |
_create_fake_setuptools_pkg_info(placeholder) |
|
296 | ||
297 |
def _create_fake_setuptools_pkg_info(placeholder): |
|
298 |
if not placeholder or not os.path.exists(placeholder): |
|
299 |
log.warn('Could not find the install location') |
|
300 |
return |
|
301 |
pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) |
|
302 |
setuptools_file = 'setuptools-%s-py%s.egg-info' % \ |
|
303 |
(SETUPTOOLS_FAKED_VERSION, pyver) |
|
304 |
pkg_info = os.path.join(placeholder, setuptools_file) |
|
305 |
if os.path.exists(pkg_info): |
|
306 |
log.warn('%s already exists', pkg_info) |
|
307 |
return |
|
308 | ||
309 |
log.warn('Creating %s', pkg_info) |
|
310 |
f = open(pkg_info, 'w') |
|
311 |
try: |
|
312 |
f.write(SETUPTOOLS_PKG_INFO) |
|
313 |
finally: |
|
314 |
f.close() |
|
315 | ||
316 |
pth_file = os.path.join(placeholder, 'setuptools.pth') |
|
317 |
log.warn('Creating %s', pth_file) |
|
318 |
f = open(pth_file, 'w') |
|
319 |
try: |
|
320 |
f.write(os.path.join(os.curdir, setuptools_file)) |
|
321 |
finally: |
|
322 |
f.close() |
|
323 | ||
324 |
_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) |
|
325 | ||
326 |
def _patch_egg_dir(path): |
|
327 |
# let's check if it's already patched |
|
328 |
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') |
|
329 |
if os.path.exists(pkg_info): |
|
330 |
if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): |
|
331 |
log.warn('%s already patched.', pkg_info) |
|
332 |
return False |
|
333 |
_rename_path(path) |
|
334 |
os.mkdir(path) |
|
335 |
os.mkdir(os.path.join(path, 'EGG-INFO')) |
|
336 |
pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') |
|
337 |
f = open(pkg_info, 'w') |
|
338 |
try: |
|
339 |
f.write(SETUPTOOLS_PKG_INFO) |
|
340 |
finally: |
|
341 |
f.close() |
|
342 |
return True |
|
343 | ||
344 |
_patch_egg_dir = _no_sandbox(_patch_egg_dir) |
|
345 | ||
346 |
def _before_install(): |
|
347 |
log.warn('Before install bootstrap.') |
|
348 |
_fake_setuptools() |
|
349 | ||
350 | ||
351 |
def _under_prefix(location): |
|
352 |
if 'install' not in sys.argv: |
|
353 |
return True |
|
354 |
args = sys.argv[sys.argv.index('install')+1:] |
|
355 |
for index, arg in enumerate(args): |
|
356 |
for option in ('--root', '--prefix'): |
|
357 |
if arg.startswith('%s=' % option): |
|
358 |
top_dir = arg.split('root=')[-1] |
|
359 |
return location.startswith(top_dir) |
|
360 |
elif arg == option: |
|
361 |
if len(args) > index: |
|
362 |
top_dir = args[index+1] |
|
363 |
return location.startswith(top_dir) |
|
364 |
if arg == '--user' and USER_SITE is not None: |
|
365 |
return location.startswith(USER_SITE) |
|
366 |
return True |
|
367 | ||
368 | ||
369 |
def _fake_setuptools(): |
|
370 |
log.warn('Scanning installed packages') |
|
371 |
try: |
|
372 |
import pkg_resources |
|
373 |
except ImportError: |
|
374 |
# we're cool |
|
375 |
log.warn('Setuptools or Distribute does not seem to be installed.') |
|
376 |
return |
|
377 |
ws = pkg_resources.working_set |
|
378 |
try: |
|
379 |
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', |
|
380 |
replacement=False)) |
|
381 |
except TypeError: |
|
382 |
# old distribute API |
|
383 |
setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) |
|
384 | ||
385 |
if setuptools_dist is None: |
|
386 |
log.warn('No setuptools distribution found') |
|
387 |
return |
|
388 |
# detecting if it was already faked |
|
389 |
setuptools_location = setuptools_dist.location |
|
390 |
log.warn('Setuptools installation detected at %s', setuptools_location) |
|
391 | ||
392 |
# if --root or --preix was provided, and if |
|
393 |
# setuptools is not located in them, we don't patch it |
|
394 |
if not _under_prefix(setuptools_location): |
|
395 |
log.warn('Not patching, --root or --prefix is installing Distribute' |
|
396 |
' in another location') |
|
397 |
return |
|
398 | ||
399 |
# let's see if its an egg |
|
400 |
if not setuptools_location.endswith('.egg'): |
|
401 |
log.warn('Non-egg installation') |
|
402 |
res = _remove_flat_installation(setuptools_location) |
|
403 |
if not res: |
|
404 |
return |
|
405 |
else: |
|
406 |
log.warn('Egg installation') |
|
407 |
pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') |
|
408 |
if (os.path.exists(pkg_info) and |
|
409 |
_same_content(pkg_info, SETUPTOOLS_PKG_INFO)): |
|
410 |
log.warn('Already patched.') |
|
411 |
return |
|
412 |
log.warn('Patching...') |
|
413 |
# let's create a fake egg replacing setuptools one |
|
414 |
res = _patch_egg_dir(setuptools_location) |
|
415 |
if not res: |
|
416 |
return |
|
417 |
log.warn('Patched done.') |
|
418 |
_relaunch() |
|
419 | ||
420 | ||
421 |
def _relaunch(): |
|
422 |
log.warn('Relaunching...') |
|
423 |
# we have to relaunch the process |
|
424 |
# pip marker to avoid a relaunch bug |
|
425 |
if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: |
|
426 |
sys.argv[0] = 'setup.py' |
|
427 |
args = [sys.executable] + sys.argv |
|
428 |
sys.exit(subprocess.call(args)) |
|
429 | ||
430 | ||
431 |
def _extractall(self, path=".", members=None): |
|
432 |
"""Extract all members from the archive to the current working |
|
433 |
directory and set owner, modification time and permissions on |
|
434 |
directories afterwards. `path' specifies a different directory |
|
435 |
to extract to. `members' is optional and must be a subset of the |
|
436 |
list returned by getmembers(). |
|
437 |
""" |
|
438 |
import copy |
|
439 |
import operator |
|
440 |
from tarfile import ExtractError |
|
441 |
directories = [] |
|
442 | ||
443 |
if members is None: |
|
444 |
members = self |
|
445 | ||
446 |
for tarinfo in members: |
|
447 |
if tarinfo.isdir(): |
|
448 |
# Extract directories with a safe mode. |
|
449 |
directories.append(tarinfo) |
|
450 |
tarinfo = copy.copy(tarinfo) |
|
451 |
tarinfo.mode = 448 # decimal for oct 0700 |
|
452 |
self.extract(tarinfo, path) |
|
453 | ||
454 |
# Reverse sort directories. |
|
455 |
if sys.version_info < (2, 4): |
|
456 |
def sorter(dir1, dir2): |
|
457 |
return cmp(dir1.name, dir2.name) |
|
458 |
directories.sort(sorter) |
|
459 |
directories.reverse() |
|
460 |
else: |
|
461 |
directories.sort(key=operator.attrgetter('name'), reverse=True) |
|
462 | ||
463 |
# Set correct owner, mtime and filemode on directories. |
|
464 |
for tarinfo in directories: |
|
465 |
dirpath = os.path.join(path, tarinfo.name) |
|
466 |
try: |
|
467 |
self.chown(tarinfo, dirpath) |
|
468 |
self.utime(tarinfo, dirpath) |
|
469 |
self.chmod(tarinfo, dirpath) |
|
470 |
except ExtractError: |
|
471 |
e = sys.exc_info()[1] |
|
472 |
if self.errorlevel > 1: |
|
473 |
raise |
|
474 |
else: |
|
475 |
self._dbg(1, "tarfile: %s" % e) |
|
476 | ||
477 | ||
478 |
def main(argv, version=DEFAULT_VERSION): |
|
479 |
"""Install or upgrade setuptools and EasyInstall""" |
|
480 |
tarball = download_setuptools() |
|
481 |
_install(tarball) |
|
482 | ||
483 | ||
484 |
if __name__ == '__main__': |
|
485 |
main(sys.argv[1:]) |
samples/authentic2-plugin-template/COPYING | ||
---|---|---|
1 |
authentic2-plugin-template is entirely under the copyright of Entr'ouvert and |
|
2 |
distributed under the license AGPLv3 or later. |
samples/authentic2-plugin-template/MANIFEST.in | ||
---|---|---|
1 |
include COPYING |
|
2 |
recursive-include src/authentic2_plugin_template/templates *.html |
|
3 |
recursive-include src/authentic2_plugin_template/static *.js *.css *.png |
samples/authentic2-plugin-template/README | ||
---|---|---|
1 |
** THIS IS A TEMPLATE PROJECT ** |
|
2 | ||
3 |
To rename it to your taste: |
|
4 | ||
5 |
$ ./adapt.sh |
|
6 | ||
7 |
** THIS IS A TEMPLATE PROJECT ** |
|
8 |
Authentic2 Plugin Template |
|
9 |
========================== |
|
10 | ||
11 |
Install |
|
12 |
------- |
|
13 | ||
14 |
You just have to install the package in your virtualenv and relaunch, it will |
|
15 |
be automatically loaded by authentic2. |
|
16 | ||
17 |
Settings |
|
18 |
-------- |
|
19 | ||
20 |
** DESCRIBE CUSTOM SETTINGS HERE ** |
samples/authentic2-plugin-template/adapt.sh | ||
---|---|---|
1 |
#!/bin/sh |
|
2 | ||
3 |
set -x |
|
4 | ||
5 |
echo "Give project name (it must match regexp ^[a-z][a-z0-9-]+$ )" |
|
6 |
read PROJECT_NAME |
|
7 | ||
8 |
if ! echo $PROJECT_NAME | grep -q '^[a-z][a-z0-9-]\+$'; then |
|
9 |
echo "Invalid project name:" $PROJECT_NAME |
|
10 |
exit 1 |
|
11 |
fi |
|
12 | ||
13 |
UPPER_UNDERSCORED=`echo $PROJECT_NAME | tr a-z A-Z | sed 's/-/_/g'` |
|
14 |
LOWER_UNDERSCORED=`echo $PROJECT_NAME | sed 's/-/_/g'` |
|
15 |
TITLECASE=`echo $PROJECT_NAME | sed 's/-/ /g;s/.*/\L&/; s/[a-z]*/\u&/g'` |
|
16 | ||
17 |
echo Project name: $PROJECT_NAME |
|
18 |
echo Uppercase underscored: $UPPER_UNDERSCORED |
|
19 |
echo Lowercase underscored: $LOWER_UNDERSCORED |
|
20 |
echo Titlecase: $TITLECASE |
|
21 | ||
22 |
if [ -d .git ]; then |
|
23 |
MV='git mv' |
|
24 |
else |
|
25 |
MV=mv |
|
26 |
fi |
|
27 | ||
28 |
sed -i \ |
|
29 |
-e "s/authentic2_plugin_template/$LOWER_UNDERSCORED/g" \ |
|
30 |
-e "s/authentic2-plugin-template/$PROJECT_NAME/g" \ |
|
31 |
-e "s/A2_TEMPLATE_/A2_$UPPER_UNDERSCORED_/g" \ |
|
32 |
-e "s/Authentic2 Plugin Template/$TITLECASE/g" \ |
|
33 |
setup.py src/*/*.py README COPYING MANIFEST.in |
|
34 |
$MV src/authentic2_plugin_template/static/authentic2_plugin_template \ |
|
35 |
src/authentic2_plugin_template/static/$LOWER_UNDERSCORED |
|
36 |
$MV src/authentic2_plugin_template/templates/authentic2_plugin_template \ |
|
37 |
src/authentic2_plugin_template/templates/$LOWER_UNDERSCORED |
|
38 |
$MV src/authentic2_plugin_template src/$LOWER_UNDERSCORED |
samples/authentic2-plugin-template/setup.py | ||
---|---|---|
1 |
#!/usr/bin/python |
|
2 |
import subprocess |
|
3 |
from setuptools import setup, find_packages |
|
4 |
import os |
|
5 | ||
6 |
def get_version(): |
|
7 |
'''Use the VERSION, if absent generates a version with git describe, if not |
|
8 |
tag exists, take 0.0.0- and add the length of the commit log. |
|
9 |
''' |
|
10 |
if os.path.exists('VERSION'): |
|
11 |
with open('VERSION', 'r') as v: |
|
12 |
return v.read() |
|
13 |
if os.path.exists('.git'): |
|
14 |
p = subprocess.Popen(['git','describe','--dirty','--match=v*'], |
|
15 |
stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
16 |
result = p.communicate()[0] |
|
17 |
if p.returncode == 0: |
|
18 |
return result.split()[0][1:].replace('-', '.') |
|
19 |
else: |
|
20 |
return '0.0.0-%s' % len( |
|
21 |
subprocess.check_output( |
|
22 |
['git', 'rev-list', 'HEAD']).splitlines()) |
|
23 |
return '0.0.0' |
|
24 | ||
25 |
README = file(os.path.join( |
|
26 |
os.path.dirname(__file__), |
|
27 |
'README')).read() |
|
28 | ||
29 |
setup(name='authentic2-plugin-template', |
|
30 |
version=get_version(), |
|
31 |
license='AGPLv3', |
|
32 |
description='Authentic2 Plugin Template', |
|
33 |
long_description=README, |
|
34 |
author="Entr'ouvert", |
|
35 |
author_email="info@entrouvert.com", |
|
36 |
packages=find_packages('src'), |
|
37 |
package_dir={ |
|
38 |
'': 'src', |
|
39 |
}, |
|
40 |
package_data={ |
|
41 |
'authentic2_plugin_template': [ |
|
42 |
'templates/authentic2_plugin_template/*.html', |
|
43 |
'static/authentic2_plugin_template/js/*.js', |
|
44 |
'static/authentic2_plugin_template/css/*.css', |
|
45 |
'static/authentic2_plugin_template/img/*.png', |
|
46 |
], |
|
47 |
}, |
|
48 |
install_requires=[ |
|
49 |
], |
|
50 |
entry_points={ |
|
51 |
'authentic2.plugin': [ |
|
52 |
'authentic2-plugin-template= authentic2_plugin_template:Plugin', |
|
53 |
], |
|
54 |
}, |
|
55 |
) |
samples/authentic2-plugin-template/src/authentic2_plugin_template/__init__.py | ||
---|---|---|
1 |
__version__ = '1.0.0' |
|
2 | ||
3 |
class Plugin(object): |
|
4 |
def get_before_urls(self): |
|
5 |
from . import urls |
|
6 |
return urls.urlpatterns |
|
7 | ||
8 |
def get_after_urls(self): |
|
9 |
return [] |
|
10 | ||
11 |
def get_apps(self): |
|
12 |
return [__name__] |
|
13 | ||
14 |
def get_before_middleware(self): |
|
15 |
return [] |
|
16 | ||
17 |
def get_after_middleware(self): |
|
18 |
return [] |
|
19 | ||
20 |
def get_authentication_backends(self): |
|
21 |
return [] |
|
22 | ||
23 |
def get_auth_frontends(self): |
|
24 |
return [] |
|
25 | ||
26 |
def get_idp_backends(self): |
|
27 |
return [] |
|
28 | ||
29 |
def service_list(self, request): |
|
30 |
'''For IdP plugins this method add links to the user homepage. |
|
31 | ||
32 |
It must return a list of authentic2.utils.Service objects, each |
|
33 |
object has a name and can have an url and some actions. |
|
34 | ||
35 |
Service(name=name[, url=url[, actions=actions]]) |
|
36 | ||
37 |
Actions are a list of tuples, whose parts are |
|
38 |
- first the name of the action, |
|
39 |
- the HTTP method for calling the action, |
|
40 |
- the URL for calling the action, |
|
41 |
- the paramters to pass to this URL as a sequence of key-value tuples. |
|
42 |
''' |
|
43 |
return [] |
|
44 | ||
45 |
def logout_list(self, request): |
|
46 |
'''For IdP or SP plugins this method add actions to logout from remote |
|
47 |
IdP or SP. |
|
48 | ||
49 |
It must returns a list of HTML fragments, each fragment is |
|
50 |
responsible for calling the view doing the logout. Views are usually |
|
51 |
called using <img/> or <iframge/> tags and finally redirect to an |
|
52 |
icon indicating success or failure for the logout. |
|
53 | ||
54 |
Authentic2 provide two such icons through the following URLs: |
|
55 |
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png') |
|
56 |
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png') |
|
57 |
''' |
|
58 |
return [] |
samples/authentic2-plugin-template/src/authentic2_plugin_template/admin.py | ||
---|---|---|
1 |
from django.contrib import admin |
|
2 | ||
3 |
from . import models |
|
4 | ||
5 |
# registrer your admin editable models here using admin.register |
samples/authentic2-plugin-template/src/authentic2_plugin_template/app_settings.py | ||
---|---|---|
1 |
class AppSettings(object): |
|
2 |
__DEFAULTS = { |
|
3 |
'ENABLE': True, |
|
4 |
} |
|
5 | ||
6 |
def __init__(self, prefix): |
|
7 |
self.prefix = prefix |
|
8 | ||
9 |
def _setting(self, name, dflt): |
|
10 |
from django.conf import settings |
|
11 |
return getattr(settings, self.prefix+name, dflt) |
|
12 | ||
13 |
def __getattr__(self, name): |
|
14 |
if name not in self.__DEFAULTS: |
|
15 |
raise AttributeError(name) |
|
16 |
return self._setting(name, self.__DEFAULTS[name]) |
|
17 | ||
18 |
# Ugly? Guido recommends this himself ... |
|
19 |
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html |
|
20 |
import sys |
|
21 |
app_settings = AppSettings('A2_PLUGIN_TEMPLATE_') |
|
22 |
app_settings.__name__ = __name__ |
|
23 |
sys.modules[__name__] = app_settings |
samples/authentic2-plugin-template/src/authentic2_plugin_template/forms.py | ||
---|---|---|
1 |
from django import forms |
|
2 | ||
3 |
samples/authentic2-plugin-template/src/authentic2_plugin_template/models.py | ||
---|---|---|
1 |
from django.db import models |
|
2 |
from django.utils.translation import ugettext_lazy as _ |
|
3 | ||
4 |
# put your models here |
samples/authentic2-plugin-template/src/authentic2_plugin_template/templates/authentic2_plugin_template/index.html | ||
---|---|---|
1 |
{% comment %}placeholder{% endcomment %} |
samples/authentic2-plugin-template/src/authentic2_plugin_template/urls.py | ||
---|---|---|
1 |
from django.conf.urls import url |
|
2 | ||
3 |
from authentic2.decorators import setting_enabled, required |
|
4 | ||
5 |
from . import app_settings |
|
6 |
from .views import index |
|
7 | ||
8 |
urlpatterns = required( |
|
9 |
setting_enabled('ENABLE', settings=app_settings), |
|
10 |
[url('^authentic2_plugin_template/$', index, name='authentic2-plugin-template-index')] |
|
11 |
) |
samples/authentic2-plugin-template/src/authentic2_plugin_template/views.py | ||
---|---|---|
1 |
from django.shortcuts import render |
|
2 | ||
3 | ||
4 |
from . import decorators |
|
5 | ||
6 |
__ALL_ = [ 'sso' ] |
|
7 | ||
8 |
@decorators.plugin_enabled |
|
9 |
def index(request): |
|
10 |
return render(request, 'authentic2_plugin_template/index.html') |
src/authentic2/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
default_app_config = 'authentic2.apps.Authentic2Config' |
src/authentic2/a2_rbac/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
default_app_config = 'authentic2.a2_rbac.apps.Authentic2RBACConfig' |
src/authentic2/a2_rbac/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib import admin |
2 | 18 |
from django.utils.translation import ugettext_lazy as _ |
3 | 19 |
from django.utils import six |
src/authentic2/a2_rbac/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 | ||
19 | ||
1 | 20 |
class AppSettings(object): |
2 | 21 |
__DEFAULTS = dict( |
3 | 22 |
MANAGED_CONTENT_TYPES=None, |
... | ... | |
21 | 40 | |
22 | 41 |
# Ugly? Guido recommends this himself ... |
23 | 42 |
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html |
24 |
import sys |
|
25 | 43 |
app_settings = AppSettings('A2_RBAC_') |
26 | 44 |
app_settings.__name__ = __name__ |
27 | 45 |
sys.modules[__name__] = app_settings |
src/authentic2/a2_rbac/apps.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.apps import AppConfig |
2 | 18 | |
3 | 19 | |
... | ... | |
7 | 23 | |
8 | 24 |
def ready(self): |
9 | 25 |
from . import signal_handlers, models |
10 |
from django.db.models.signals import post_save, post_migrate, pre_save, \ |
|
11 |
post_delete |
|
12 |
from django.contrib.contenttypes.models import ContentType |
|
26 |
from django.db.models.signals import post_save, post_migrate, post_delete |
|
13 | 27 |
from authentic2.models import Service |
14 | 28 | |
15 | 29 |
# update rbac on save to contenttype, ou and roles |
src/authentic2/a2_rbac/fields.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db.models import NullBooleanField |
2 | 18 |
from django import forms |
3 | 19 | |
20 | ||
4 | 21 |
class UniqueBooleanField(NullBooleanField): |
5 | 22 |
'''BooleanField allowing only one True value in the table, and preventing |
6 | 23 |
problems with multiple False values by implicitely converting them to |
src/authentic2/a2_rbac/management.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils import six |
2 | 18 |
from django.utils.translation import ugettext_lazy as _ |
3 | 19 |
from django.utils.text import slugify |
4 | 20 |
from django.contrib.contenttypes.models import ContentType |
5 |
from django.db.models.signals import post_migrate |
|
6 |
from django.apps import apps |
|
7 | 21 | |
8 | 22 |
from django_rbac.utils import get_role_model, get_ou_model, \ |
9 | 23 |
get_permission_model |
10 | 24 | |
11 | 25 |
from ..utils import get_fk_model |
12 |
from . import utils, app_settings, signal_handlers
|
|
26 |
from . import utils, app_settings |
|
13 | 27 | |
14 | 28 | |
15 | 29 |
def update_ou_admin_roles(ou): |
... | ... | |
59 | 73 |
they give general administrative rights to all mamanged content types |
60 | 74 |
scoped to the given organizational unit. |
61 | 75 |
''' |
62 |
Role = get_role_model() |
|
63 |
Permission = get_permission_model() |
|
64 | 76 |
OU = get_ou_model() |
65 | 77 |
ou_all = OU.objects.all() |
66 | 78 |
if len(ou_all) < 2: |
src/authentic2/a2_rbac/managers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib.contenttypes.models import ContentType |
2 | 18 | |
3 | 19 |
from django_rbac.models import ADMIN_OP |
... | ... | |
60 | 76 |
defaults={ |
61 | 77 |
'name': name, |
62 | 78 |
'slug': slug, |
63 |
}, **kwargs) |
|
79 |
}, |
|
80 |
**kwargs) |
|
64 | 81 |
if update_name and not created and role.name != name: |
65 | 82 |
role.name = name |
66 | 83 |
role.save() |
src/authentic2/a2_rbac/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from collections import namedtuple |
2 | 18 |
from django.core.exceptions import ValidationError |
3 | 19 |
from django.utils import six |
... | ... | |
5 | 21 |
from django.utils.text import slugify |
6 | 22 |
from django.db import models |
7 | 23 |
from django.contrib.contenttypes.models import ContentType |
8 |
from django.core.validators import RegexValidator |
|
9 | 24 | |
10 | 25 |
from django_rbac.models import (RoleAbstractBase, PermissionAbstractBase, |
11 | 26 |
OrganizationalUnitAbstractBase, RoleParentingAbstractBase, VIEW_OP, |
... | ... | |
32 | 47 |
MANUAL_PASSWORD_POLICY = 1 |
33 | 48 | |
34 | 49 |
USER_ADD_PASSWD_POLICY_CHOICES = ( |
35 |
(RESET_LINK_POLICY, _('Send reset link')),
|
|
36 |
(MANUAL_PASSWORD_POLICY, _('Manual password definition')),
|
|
50 |
(RESET_LINK_POLICY, _('Send reset link')), |
|
51 |
(MANUAL_PASSWORD_POLICY, _('Manual password definition')), |
|
37 | 52 |
) |
38 | 53 | |
39 | 54 |
PolicyValue = namedtuple('PolicyValue', [ |
40 |
'generate_password', 'reset_password_at_next_login',
|
|
41 |
'send_mail', 'send_password_reset'])
|
|
55 |
'generate_password', 'reset_password_at_next_login', |
|
56 |
'send_mail', 'send_password_reset']) |
|
42 | 57 | |
43 | 58 |
USER_ADD_PASSWD_POLICY_VALUES = { |
44 |
RESET_LINK_POLICY: PolicyValue(False, False, False, True),
|
|
45 |
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
|
|
59 |
RESET_LINK_POLICY: PolicyValue(False, False, False, True), |
|
60 |
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False), |
|
46 | 61 |
} |
47 | 62 | |
48 | 63 |
username_is_unique = models.BooleanField( |
... | ... | |
247 | 262 |
) |
248 | 263 | |
249 | 264 |
def natural_key(self): |
250 |
return [self.slug, self.ou and self.ou.natural_key(), self.service and |
|
251 |
self.service.natural_key()] |
|
265 |
return [ |
|
266 |
self.slug, |
|
267 |
self.ou and self.ou.natural_key(), |
|
268 |
self.service and self.service.natural_key(), |
|
269 |
] |
|
252 | 270 | |
253 | 271 |
def to_json(self): |
254 | 272 |
return { |
src/authentic2/a2_rbac/signal_handlers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import ugettext as _ |
2 | 18 |
from django.conf import settings |
3 | 19 |
from django.apps import apps |
... | ... | |
8 | 24 |
from django_rbac.utils import get_ou_model, get_role_model, get_operation |
9 | 25 |
from django_rbac.managers import defer_update_transitive_closure |
10 | 26 | |
27 | ||
11 | 28 |
def create_default_ou(app_config, verbosity=2, interactive=True, |
12 | 29 |
using=DEFAULT_DB_ALIAS, **kwargs): |
13 | 30 |
if not router.allow_migrate(using, get_ou_model()): |
... | ... | |
36 | 53 |
def post_migrate_update_rbac(app_config, verbosity=2, interactive=True, |
37 | 54 |
using=DEFAULT_DB_ALIAS, **kwargs): |
38 | 55 |
# be sure new objects names are localized using the default locale |
39 |
from .management import update_ou_admin_roles, update_ous_admin_roles, \ |
|
40 |
update_content_types_roles |
|
41 | ||
56 |
from .management import update_ous_admin_roles, update_content_types_roles |
|
42 | 57 | |
43 | 58 |
if not router.allow_migrate(using, get_role_model()): |
44 | 59 |
return |
... | ... | |
50 | 65 | |
51 | 66 | |
52 | 67 |
def update_rbac_on_ou_post_save(sender, instance, created, raw, **kwargs): |
53 |
from .management import update_ou_admin_roles, update_ous_admin_roles, \
|
|
54 |
update_content_types_roles |
|
68 |
from .management import update_ou_admin_roles, update_ous_admin_roles |
|
69 | ||
55 | 70 |
if get_ou_model().objects.count() < 3 and created: |
56 | 71 |
update_ous_admin_roles() |
57 | 72 |
else: |
58 | 73 |
update_ou_admin_roles(instance) |
59 | 74 | |
75 | ||
60 | 76 |
def update_rbac_on_ou_post_delete(sender, instance, **kwargs): |
61 |
from .management import update_ou_admin_roles, update_ous_admin_roles, \
|
|
62 |
update_content_types_roles |
|
77 |
from .management import update_ous_admin_roles
|
|
78 | ||
63 | 79 |
if get_ou_model().objects.count() < 2: |
64 | 80 |
update_ous_admin_roles() |
65 | 81 |
src/authentic2/a2_rbac/tests.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.test import TestCase |
2 | 18 |
from django.contrib.contenttypes.models import ContentType |
3 | 19 |
from django.contrib.auth import get_user_model |
... | ... | |
10 | 26 |
User = get_user_model() |
11 | 27 | |
12 | 28 | |
13 | ||
14 | 29 |
class A2RBACTestCase(TestCase): |
15 | 30 |
def test_update_rbac(self): |
16 | 31 |
# 3 content types managers and 1 global manager |
... | ... | |
73 | 88 |
self.assertTrue(role.slug.startswith('_a2'), u'role %s slug must ' |
74 | 89 |
'start with _a2: %s' % (role.name, role.slug)) |
75 | 90 | |
76 | ||
77 | 91 |
def test_admin_roles_update_slug(self): |
78 | 92 |
user = User.objects.create(username='john.doe') |
79 | 93 |
name1 = 'Can manage john.doe' |
src/authentic2/a2_rbac/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib.auth import get_user_model |
2 | 18 |
from django.contrib.contenttypes.models import ContentType |
3 | 19 |
from django_rbac.models import VIEW_OP, SEARCH_OP |
src/authentic2/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from copy import deepcopy |
2 | 18 |
import pprint |
3 | 19 | |
... | ... | |
5 | 21 |
from django.conf import settings |
6 | 22 |
from django.utils.translation import ugettext_lazy as _ |
7 | 23 |
from django.utils import timezone |
8 |
from django.utils.http import urlencode |
|
9 |
from django.http import HttpResponseRedirect |
|
10 | 24 |
from django.views.decorators.cache import never_cache |
11 | 25 |
from django.contrib.auth.admin import UserAdmin |
12 | 26 |
from django.contrib.sessions.models import Session |
13 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
|
14 | 27 |
from django.contrib.admin.utils import flatten_fieldsets |
15 | 28 |
from django import forms |
16 | 29 |
from django.contrib.auth.forms import ReadOnlyPasswordHashField |
17 | 30 | |
18 | 31 |
from .nonce.models import Nonce |
19 |
from . import (models, compat, app_settings, decorators,
|
|
20 |
attribute_kinds, utils)
|
|
21 |
from .forms import modelform_factory, BaseUserForm
|
|
32 |
from . import (models, app_settings, decorators, attribute_kinds,
|
|
33 |
utils)
|
|
34 |
from .forms.profile import BaseUserForm, modelform_factory
|
|
22 | 35 |
from .custom_user.models import User |
23 | 36 | |
37 | ||
24 | 38 |
def cleanup_action(modeladmin, request, queryset): |
25 | 39 |
queryset.cleanup() |
26 | 40 |
cleanup_action.short_description = _('Cleanup expired objects') |
27 | 41 | |
42 | ||
28 | 43 |
class CleanupAdminMixin(admin.ModelAdmin): |
29 | 44 |
def get_actions(self, request): |
30 | 45 |
actions = super(CleanupAdminMixin, self).get_actions(request) |
... | ... | |
32 | 47 |
actions['cleanup_action'] = cleanup_action, 'cleanup_action', cleanup_action.short_description |
33 | 48 |
return actions |
34 | 49 | |
50 | ||
35 | 51 |
class NonceModelAdmin(admin.ModelAdmin): |
36 | 52 |
list_display = ("value", "context", "not_on_or_after") |
53 | ||
37 | 54 |
admin.site.register(Nonce, NonceModelAdmin) |
55 | ||
56 | ||
38 | 57 |
class AttributeValueAdmin(admin.ModelAdmin): |
39 |
list_display = ('content_type', 'owner', 'attribute', |
|
40 |
'content') |
|
58 |
list_display = ('content_type', 'owner', 'attribute', 'content')
|
|
59 | ||
41 | 60 |
admin.site.register(models.AttributeValue, AttributeValueAdmin) |
61 | ||
62 | ||
42 | 63 |
class LogoutUrlAdmin(admin.ModelAdmin): |
43 | 64 |
list_display = ('provider', 'logout_url', 'logout_use_iframe', 'logout_use_iframe_timeout') |
65 | ||
44 | 66 |
admin.site.register(models.LogoutUrl, LogoutUrlAdmin) |
67 | ||
68 | ||
45 | 69 |
class AuthenticationEventAdmin(admin.ModelAdmin): |
46 | 70 |
list_display = ('when', 'who', 'how', 'nonce') |
47 | 71 |
list_filter = ('how',) |
... | ... | |
49 | 73 |
search_fields = ('who', 'nonce', 'how') |
50 | 74 | |
51 | 75 |
admin.site.register(models.AuthenticationEvent, AuthenticationEventAdmin) |
76 | ||
77 | ||
52 | 78 |
class UserExternalIdAdmin(admin.ModelAdmin): |
53 | 79 |
list_display = ('user', 'source', 'external_id', 'created', 'updated') |
54 | 80 |
list_filter = ('source',) |
55 | 81 |
date_hierarchy = 'created' |
56 | 82 |
search_fields = ('user__username', 'source', 'external_id') |
83 | ||
57 | 84 |
admin.site.register(models.UserExternalId, UserExternalIdAdmin) |
85 | ||
86 | ||
58 | 87 |
class DeletedUserAdmin(admin.ModelAdmin): |
59 | 88 |
list_display = ('user', 'creation') |
60 | 89 |
date_hierarchy = 'creation' |
... | ... | |
96 | 125 |
backend = auth.load_backend(backend_class) |
97 | 126 |
try: |
98 | 127 |
user = backend.get_user(user_id) or auth_models.AnonymousUser() |
99 |
except: |
|
128 |
except Exception:
|
|
100 | 129 |
user = _('deleted user %r') % user_id |
101 | 130 |
return user |
102 | 131 |
user.short_description = _('user') |
... | ... | |
107 | 136 | |
108 | 137 |
admin.site.register(Session, SessionAdmin) |
109 | 138 | |
139 | ||
110 | 140 |
class ExternalUserListFilter(admin.SimpleListFilter): |
111 | 141 |
title = _('external') |
112 | 142 | |
... | ... | |
114 | 144 | |
115 | 145 |
def lookups(self, request, model_admin): |
116 | 146 |
return ( |
117 |
('1', _('Yes')),
|
|
118 |
('0', _('No'))
|
|
147 |
('1', _('Yes')), |
|
148 |
('0', _('No')) |
|
119 | 149 |
) |
120 | 150 | |
121 | 151 |
def queryset(self, request, queryset): |
... | ... | |
130 | 160 |
return queryset.filter(userexternalid__isnull=True) |
131 | 161 |
return queryset |
132 | 162 | |
163 | ||
133 | 164 |
class UserRealmListFilter(admin.SimpleListFilter): |
134 | 165 |
# Human-readable title which will be displayed in the |
135 | 166 |
# right admin sidebar just above the filter options. |
... | ... | |
164 | 195 |
'missing_credential': _("You must at least give a username or an email to your user"), |
165 | 196 |
} |
166 | 197 | |
167 |
password = ReadOnlyPasswordHashField(label=_("Password"), |
|
198 |
password = ReadOnlyPasswordHashField( |
|
199 |
label=_("Password"), |
|
168 | 200 |
help_text=_("Raw passwords are not stored, so there is no way to see " |
169 | 201 |
"this user's password, but you can change the password " |
170 | 202 |
"using <a href=\"password/\">this form</a>.")) |
... | ... | |
192 | 224 |
code='missing_credential', |
193 | 225 |
) |
194 | 226 | |
227 | ||
195 | 228 |
class UserCreationForm(BaseUserForm): |
196 | 229 |
""" |
197 | 230 |
A form that creates a user, with no privileges, from the given username and |
... | ... | |
201 | 234 |
'password_mismatch': _("The two password fields didn't match."), |
202 | 235 |
'missing_credential': _("You must at least give a username or an email to your user"), |
203 | 236 |
} |
204 |
password1 = forms.CharField(label=_("Password"), |
|
237 |
password1 = forms.CharField( |
|
238 |
label=_("Password"), |
|
205 | 239 |
widget=forms.PasswordInput) |
206 |
password2 = forms.CharField(label=_("Password confirmation"), |
|
240 |
password2 = forms.CharField( |
|
241 |
label=_("Password confirmation"), |
|
207 | 242 |
widget=forms.PasswordInput, |
208 | 243 |
help_text=_("Enter the same password as above, for verification.")) |
209 | 244 | |
... | ... | |
235 | 270 |
user.save() |
236 | 271 |
return user |
237 | 272 | |
273 | ||
238 | 274 |
class AuthenticUserAdmin(UserAdmin): |
239 | 275 |
fieldsets = ( |
240 | 276 |
(None, {'fields': ('uuid', 'ou', 'password')}), |
... | ... | |
244 | 280 |
(_('Important dates'), {'fields': ('last_login', 'date_joined')}), |
245 | 281 |
) |
246 | 282 |
add_fieldsets = ( |
247 |
(None, { |
|
248 |
'classes': ('wide',), |
|
249 |
'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2')} |
|
250 |
), |
|
251 |
) |
|
283 |
(None, { |
|
284 |
'classes': ('wide',), |
|
285 |
'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2')}), |
|
286 |
) |
|
252 | 287 |
readonly_fields = ('uuid',) |
253 |
list_filter = UserAdmin.list_filter + (UserRealmListFilter,ExternalUserListFilter) |
|
288 |
list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter)
|
|
254 | 289 |
list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email'] |
255 | 290 | |
256 | 291 |
def get_fieldsets(self, request, obj=None): |
257 | 292 |
fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj)) |
258 | 293 |
if obj: |
259 | 294 |
if not request.user.is_superuser: |
260 |
fieldsets[2][1]['fields'] = filter(lambda x: x != |
|
261 |
'is_superuser', fieldsets[2][1]['fields']) |
|
295 |
fieldsets[2][1]['fields'] = filter(lambda x: x != 'is_superuser', fieldsets[2][1]['fields']) |
|
262 | 296 |
qs = models.Attribute.objects.all() |
263 | 297 |
insertion_idx = 2 |
264 | 298 |
else: |
... | ... | |
292 | 326 |
kwargs['fields'] = fields |
293 | 327 |
return super(AuthenticUserAdmin, self).get_form(request, obj=obj, **kwargs) |
294 | 328 | |
329 |
admin.site.register(User, AuthenticUserAdmin) |
|
330 | ||
295 | 331 | |
296 | 332 |
class AttributeForm(forms.ModelForm): |
297 | 333 |
def __init__(self, *args, **kwargs): |
... | ... | |
318 | 354 |
def get_queryset(self, request): |
319 | 355 |
return self.model.all_objects.all() |
320 | 356 | |
321 | ||
322 | 357 |
admin.site.register(models.Attribute, AttributeAdmin) |
323 | 358 | |
324 | 359 | |
... | ... | |
328 | 363 | |
329 | 364 |
admin.site.login = login |
330 | 365 | |
366 | ||
331 | 367 |
@never_cache |
332 | 368 |
def logout(request, extra_context=None): |
333 | 369 |
return utils.redirect_to_login(request, login_url='auth_logout') |
... | ... | |
335 | 371 |
admin.site.logout = logout |
336 | 372 | |
337 | 373 |
admin.site.register(models.PasswordReset) |
338 |
admin.site.register(User, AuthenticUserAdmin) |
src/authentic2/api_urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 | 18 | |
3 | 19 |
from . import api_views |
4 | 20 | |
5 | 21 |
urlpatterns = [ |
6 |
url(r'^register/$', api_views.register, |
|
7 |
name='a2-api-register'), |
|
8 |
url(r'^password-change/$', api_views.password_change, |
|
9 |
name='a2-api-password-change'), |
|
10 |
url(r'^user/$', api_views.user, |
|
11 |
name='a2-api-user'), |
|
12 |
url(r'^roles/(?P<role_uuid>[\w+]*)/members/(?P<member_uuid>[^/]+)/$', |
|
13 |
api_views.role_memberships, name='a2-api-role-member'), |
|
14 |
url(r'^check-password/$', api_views.check_password, |
|
15 |
name='a2-api-check-password'), |
|
16 |
url(r'^validate-password/$', api_views.validate_password, |
|
17 |
name='a2-api-validate-password'), |
|
22 |
url(r'^register/$', api_views.register, name='a2-api-register'), |
|
23 |
url(r'^password-change/$', api_views.password_change, name='a2-api-password-change'), |
|
24 |
url(r'^user/$', api_views.user, name='a2-api-user'), |
|
25 |
url(r'^roles/(?P<role_uuid>[\w+]*)/members/(?P<member_uuid>[^/]+)/$', api_views.role_memberships, |
|
26 |
name='a2-api-role-member'), |
|
27 |
url(r'^check-password/$', api_views.check_password, name='a2-api-check-password'), |
|
28 |
url(r'^validate-password/$', api_views.validate_password, name='a2-api-validate-password'), |
|
18 | 29 |
] |
19 | 30 | |
20 | 31 |
urlpatterns += api_views.router.urls |
src/authentic2/api_views.py | ||
---|---|---|
1 | 1 |
# authentic2 - versatile identity manager |
2 |
# Copyright (C) 2010-2018 Entr'ouvert
|
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert
|
|
3 | 3 |
# |
4 | 4 |
# This program is free software: you can redistribute it and/or modify it |
5 | 5 |
# under the terms of the GNU Affero General Public License as published |
... | ... | |
20 | 20 |
from django.db import models |
21 | 21 |
from django.contrib.auth import get_user_model |
22 | 22 |
from django.core.exceptions import MultipleObjectsReturned |
23 |
from django.utils import six |
|
24 | 23 |
from django.utils.translation import ugettext as _ |
25 | 24 |
from django.utils.encoding import force_text |
26 | 25 |
from django.views.decorators.vary import vary_on_headers |
... | ... | |
138 | 137 |
User.objects.filter(ou=ou, email__iexact=data['email']).exists(): |
139 | 138 |
raise serializers.ValidationError( |
140 | 139 |
_('You already have an account')) |
141 |
if (ou.username_is_unique and
|
|
142 |
'username' not in data): |
|
140 |
if (ou.username_is_unique |
|
141 |
and 'username' not in data):
|
|
143 | 142 |
raise serializers.ValidationError( |
144 | 143 |
_('Username is required in this ou')) |
145 | 144 |
if ou.username_is_unique and User.objects.filter( |
... | ... | |
779 | 778 |
result['errors'] = [exc.detail] |
780 | 779 |
return result, status.HTTP_200_OK |
781 | 780 | |
782 | ||
783 | 781 |
check_password = CheckPasswordAPI.as_view() |
784 | 782 | |
785 | 783 | |
... | ... | |
811 | 809 |
result['ok'] = ok |
812 | 810 |
return result, status.HTTP_200_OK |
813 | 811 | |
814 | ||
815 | 812 |
validate_password = ValidatePasswordAPI.as_view() |
src/authentic2/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import sys |
2 | 18 |
import six |
3 | 19 | |
... | ... | |
19 | 35 |
def has_default(self): |
20 | 36 |
return self.default != self.SENTINEL |
21 | 37 | |
38 | ||
22 | 39 |
class AppSettings(object): |
23 | 40 |
def __init__(self, defaults): |
24 | 41 |
self.defaults = defaults |
... | ... | |
35 | 52 |
realms = {} |
36 | 53 |
if self.A2_REGISTRATION_REALM: |
37 | 54 |
realms[self.A2_REGISTRATION_REALM] = self.A2_REGISTRATION_REALM |
55 | ||
38 | 56 |
def add_realms(new_realms): |
39 | 57 |
for realm in new_realms: |
40 | 58 |
if not isinstance(realm, (tuple, list)): |
... | ... | |
68 | 86 |
return getattr(self.settings, other_key) |
69 | 87 |
if self.defaults[key].has_default(): |
70 | 88 |
return self.defaults[key].default |
71 |
raise ImproperlyConfigured('missing setting %s(%s) is mandatory' %
|
|
72 |
(key, self.defaults[key].description))
|
|
89 |
raise ImproperlyConfigured( |
|
90 |
'missing setting %s(%s) is mandatory' % (key, self.defaults[key].description))
|
|
73 | 91 | |
74 | ||
75 |
# Registration |
|
76 | 92 |
default_settings = dict( |
77 |
ATTRIBUTE_BACKENDS = Setting(
|
|
93 |
ATTRIBUTE_BACKENDS=Setting(
|
|
78 | 94 |
names=('A2_ATTRIBUTE_BACKENDS',), |
79 |
default=('authentic2.attributes_ng.sources.format', |
|
80 |
'authentic2.attributes_ng.sources.function', |
|
81 |
'authentic2.attributes_ng.sources.django_user', |
|
82 |
'authentic2.attributes_ng.sources.ldap', |
|
83 |
'authentic2.attributes_ng.sources.computed_targeted_id', |
|
84 |
'authentic2.attributes_ng.sources.service_roles', |
|
95 |
default=( |
|
96 |
'authentic2.attributes_ng.sources.format', |
|
97 |
'authentic2.attributes_ng.sources.function', |
|
98 |
'authentic2.attributes_ng.sources.django_user', |
|
99 |
'authentic2.attributes_ng.sources.ldap', |
|
100 |
'authentic2.attributes_ng.sources.computed_targeted_id', |
|
101 |
'authentic2.attributes_ng.sources.service_roles', |
|
85 | 102 |
), |
86 | 103 |
definition='List of attribute backend classes or modules', |
87 | 104 |
), |
88 |
CAFILE = Setting(names=('AUTHENTIC2_CAFILE', 'CAFILE'), |
|
89 |
default=None, |
|
90 |
definition='File containing certificate chains as PEM certificates'), |
|
91 |
A2_REGISTRATION_URLCONF = Setting(default='authentic2.registration_backend.urls', |
|
92 |
definition='Root urlconf for the /accounts endpoints'), |
|
93 |
A2_REGISTRATION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationForm', |
|
94 |
definition='Default registration form'), |
|
95 |
A2_REGISTRATION_COMPLETION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationCompletionForm', |
|
96 |
definition='Default registration completion form'), |
|
97 |
A2_REGISTRATION_SET_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.SetPasswordForm', |
|
98 |
definition='Default set password form'), |
|
99 |
A2_REGISTRATION_CHANGE_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.PasswordChangeForm', |
|
100 |
definition='Default change password form'), |
|
101 |
A2_REGISTRATION_CAN_DELETE_ACCOUNT = Setting(default=True, |
|
102 |
definition='Can user self delete their account and all their data'), |
|
103 |
A2_REGISTRATION_CAN_CHANGE_PASSWORD = Setting(default=True, definition='Allow user to change its own password'), |
|
104 |
A2_REGISTRATION_EMAIL_BLACKLIST = Setting(default=[], definition='List of forbidden email ' |
|
105 |
'wildcards, ex.: ^.*@ville.fr$'), |
|
106 |
A2_REGISTRATION_REDIRECT = Setting(default=None, definition='Forced redirection after each redirect, NEXT_URL ' |
|
107 |
' substring is replaced by the original next_url passed to /accounts/register/'), |
|
108 |
A2_PROFILE_CAN_CHANGE_EMAIL = Setting(default=True, |
|
109 |
definition='Can user self change their email'), |
|
110 |
A2_PROFILE_CAN_EDIT_PROFILE = Setting(default=True, |
|
111 |
definition='Can user self edit their profile'), |
|
112 |
A2_PROFILE_CAN_MANAGE_FEDERATION = Setting(default=True, |
|
113 |
definition='Can user manage its federations'), |
|
114 |
A2_PROFILE_DISPLAY_EMPTY_FIELDS = Setting(default=False, |
|
115 |
definition='Include empty fields in profile view'), |
|
116 |
A2_HOMEPAGE_URL = Setting(default=None, definition='IdP has no homepage, ' |
|
117 |
'redirect to this one.'), |
|
118 |
A2_USER_CAN_RESET_PASSWORD = Setting(default=None, definition='Allow online reset of passwords'), |
|
119 |
A2_EMAIL_IS_UNIQUE = Setting(default=False, |
|
105 |
CAFILE=Setting( |
|
106 |
names=('AUTHENTIC2_CAFILE', 'CAFILE'), |
|
107 |
default=None, |
|
108 |
definition='File containing certificate chains as PEM certificates'), |
|
109 |
A2_REGISTRATION_CAN_DELETE_ACCOUNT=Setting( |
|
110 |
default=True, |
|
111 |
definition='Can user self delete their account and all their data'), |
|
112 |
A2_REGISTRATION_CAN_CHANGE_PASSWORD=Setting( |
|
113 |
default=True, |
|
114 |
definition='Allow user to change its own password'), |
|
115 |
A2_REGISTRATION_EMAIL_BLACKLIST=Setting( |
|
116 |
default=[], |
|
117 |
definition='List of forbidden email wildcards, ex.: ^.*@ville.fr$'), |
|
118 |
A2_REGISTRATION_REDIRECT=Setting( |
|
119 |
default=None, |
|
120 |
definition='Forced redirection after each redirect, NEXT_URL substring is replaced' |
|
121 |
' by the original next_url passed to /accounts/register/'), |
|
122 |
A2_PROFILE_CAN_CHANGE_EMAIL=Setting( |
|
123 |
default=True, |
|
124 |
definition='Can user self change their email'), |
|
125 |
A2_PROFILE_CAN_EDIT_PROFILE=Setting( |
|
126 |
default=True, |
|
127 |
definition='Can user self edit their profile'), |
|
128 |
A2_PROFILE_CAN_MANAGE_FEDERATION=Setting( |
|
129 |
default=True, |
|
130 |
definition='Can user manage its federations'), |
|
131 |
A2_PROFILE_DISPLAY_EMPTY_FIELDS=Setting( |
|
132 |
default=False, |
|
133 |
definition='Include empty fields in profile view'), |
|
134 |
A2_HOMEPAGE_URL=Setting( |
|
135 |
default=None, |
|
136 |
definition='IdP has no homepage, redirect to this one.'), |
|
137 |
A2_USER_CAN_RESET_PASSWORD=Setting( |
|
138 |
default=None, |
|
139 |
definition='Allow online reset of passwords'), |
|
140 |
A2_EMAIL_IS_UNIQUE=Setting( |
|
141 |
default=False, |
|
120 | 142 |
definition='Email of users must be unique'), |
121 |
A2_REGISTRATION_EMAIL_IS_UNIQUE = Setting(default=False, |
|
143 |
A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting( |
|
144 |
default=False, |
|
122 | 145 |
definition='Email of registererd accounts must be unique'), |
123 |
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting(default=r'^[\w.@+-]+$', definition='Regex to validate usernames'), |
|
124 |
A2_REGISTRATION_FORM_USERNAME_HELP_TEXT=Setting(default=_('Required. At most ' |
|
125 |
'30 characters. Letters, digits, and @/./+/-/_ only.')), |
|
126 |
A2_REGISTRATION_FORM_USERNAME_LABEL=Setting(default=_('Username')), |
|
127 |
A2_REGISTRATION_REALM=Setting(default=None, definition='Default realm to assign to self-registrated users'), |
|
128 |
A2_REGISTRATION_GROUPS=Setting(default=(), definition='Default groups for self-registered users'), |
|
129 |
A2_PROFILE_FIELDS=Setting(default=(), definition='Fields to show to the user in the profile page'), |
|
130 |
A2_REGISTRATION_FIELDS=Setting(default=(), definition='Fields from the user model that must appear on the registration form'), |
|
131 |
A2_REQUIRED_FIELDS=Setting(default=(), definition='User fields that are required'), |
|
132 |
A2_REGISTRATION_REQUIRED_FIELDS=Setting(default=(), definition='Fields from the registration form that must be required'), |
|
133 |
A2_PRE_REGISTRATION_FIELDS=Setting(default=(), definition='User fields to ask with email'), |
|
134 |
A2_REALMS=Setting(default=(), definition='List of realms to search user accounts'), |
|
135 |
A2_USERNAME_REGEX=Setting(default=None, definition='Regex that username must validate'), |
|
136 |
A2_USERNAME_LABEL=Setting(default=None, definition='Alternate username label for the login' |
|
137 |
' form'), |
|
138 |
A2_USERNAME_HELP_TEXT=Setting(default=None, definition='Help text to explain validation rules of usernames'), |
|
139 |
A2_USERNAME_IS_UNIQUE=Setting(default=True, definition='Check username uniqueness'), |
|
140 |
A2_LOGIN_FORM_OU_SELECTOR=Setting(default=False, definition='Whether to add an OU selector to the login form'), |
|
141 |
A2_LOGIN_FORM_OU_SELECTOR_LABEL=Setting(default=None, definition='Label of OU field on login page'), |
|
142 |
A2_REGISTRATION_USERNAME_IS_UNIQUE=Setting(default=True, definition='Check username uniqueness on registration'), |
|
146 |
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting( |
|
147 |
default=r'^[\w.@+-]+$', |
|
148 |
definition='Regex to validate usernames'), |
|
149 |
A2_REGISTRATION_FORM_USERNAME_HELP_TEXT=Setting( |
|
150 |
default=_('Required. At most 30 characters. Letters, digits, and @/./+/-/_ only.')), |
|
151 |
A2_REGISTRATION_FORM_USERNAME_LABEL=Setting( |
|
152 |
default=_('Username')), |
|
153 |
A2_REGISTRATION_REALM=Setting( |
|
154 |
default=None, |
|
155 |
definition='Default realm to assign to self-registrated users'), |
|
156 |
A2_REGISTRATION_GROUPS=Setting( |
|
157 |
default=(), |
|
158 |
definition='Default groups for self-registered users'), |
|
159 |
A2_PROFILE_FIELDS=Setting( |
|
160 |
default=(), |
|
161 |
definition='Fields to show to the user in the profile page'), |
|
162 |
A2_REGISTRATION_FIELDS=Setting( |
|
163 |
default=(), |
|
164 |
definition='Fields from the user model that must appear on the registration form'), |
|
165 |
A2_REQUIRED_FIELDS=Setting( |
|
166 |
default=(), |
|
167 |
definition='User fields that are required'), |
|
168 |
A2_REGISTRATION_REQUIRED_FIELDS=Setting( |
|
169 |
default=(), |
|
170 |
definition='Fields from the registration form that must be required'), |
|
171 |
A2_PRE_REGISTRATION_FIELDS=Setting( |
|
172 |
default=(), |
|
173 |
definition='User fields to ask with email'), |
|
174 |
A2_REALMS=Setting( |
|
175 |
default=(), |
|
176 |
definition='List of realms to search user accounts'), |
|
177 |
A2_USERNAME_REGEX=Setting( |
|
178 |
default=None, |
|
179 |
definition='Regex that username must validate'), |
|
180 |
A2_USERNAME_LABEL=Setting( |
|
181 |
default=None, |
|
182 |
definition='Alternate username label for the login form'), |
|
183 |
A2_USERNAME_HELP_TEXT=Setting( |
|
184 |
default=None, |
|
185 |
definition='Help text to explain validation rules of usernames'), |
|
186 |
A2_USERNAME_IS_UNIQUE=Setting( |
|
187 |
default=True, |
|
188 |
definition='Check username uniqueness'), |
|
189 |
A2_LOGIN_FORM_OU_SELECTOR=Setting( |
|
190 |
default=False, |
|
191 |
definition='Whether to add an OU selector to the login form'), |
|
192 |
A2_LOGIN_FORM_OU_SELECTOR_LABEL=Setting( |
|
193 |
default=None, |
|
194 |
definition='Label of OU field on login page'), |
|
195 |
A2_REGISTRATION_USERNAME_IS_UNIQUE=Setting( |
|
196 |
default=True, |
|
197 |
definition='Check username uniqueness on registration'), |
|
143 | 198 |
IDP_BACKENDS=(), |
144 | 199 |
AUTH_FRONTENDS=(), |
145 | 200 |
AUTH_FRONTENDS_KWARGS={}, |
146 |
VALID_REFERERS=Setting(default=(), definition='List of prefix to match referers'), |
|
147 |
A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'), |
|
148 |
A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None), |
|
149 |
A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'), |
|
150 |
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting(default=200, definition='Width and height for a profile image'), |
|
151 |
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), |
|
152 |
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), |
|
153 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), |
|
154 |
A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), |
|
155 |
A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), |
|
156 |
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), |
|
201 |
VALID_REFERERS=Setting( |
|
202 |
default=(), |
|
203 |
definition='List of prefix to match referers'), |
|
204 |
A2_OPENED_SESSION_COOKIE_NAME=Setting( |
|
205 |
default='A2_OPENED_SESSION', |
|
206 |
definition='Authentic session open'), |
|
207 |
A2_OPENED_SESSION_COOKIE_DOMAIN=Setting( |
|
208 |
default=None), |
|
209 |
A2_ATTRIBUTE_KINDS=Setting( |
|
210 |
default=(), |
|
211 |
definition='List of other attribute kinds'), |
|
212 |
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting( |
|
213 |
default=200, |
|
214 |
definition='Width and height for a profile image'), |
|
215 |
A2_VALIDATE_EMAIL=Setting( |
|
216 |
default=False, |
|
217 |
definition='Validate user email server by doing an RCPT command'), |
|
218 |
A2_VALIDATE_EMAIL_DOMAIN=Setting( |
|
219 |
default=True, |
|
220 |
definition='Validate user email domain'), |
|
221 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting( |
|
222 |
default=3, |
|
223 |
definition='Minimum number of characters classes to be present in passwords'), |
|
224 |
A2_PASSWORD_POLICY_MIN_LENGTH=Setting( |
|
225 |
default=8, |
|
226 |
definition='Minimum number of characters in a password'), |
|
227 |
A2_PASSWORD_POLICY_REGEX=Setting( |
|
228 |
default=None, |
|
229 |
definition='Regular expression for validating passwords'), |
|
230 |
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting( |
|
231 |
default=None, |
|
232 |
definition='Error message to show when the password do not validate the regular expression'), |
|
157 | 233 |
A2_PASSWORD_POLICY_CLASS=Setting( |
158 | 234 |
default='authentic2.passwords.DefaultPasswordChecker', |
159 | 235 |
definition='path of a class to validate passwords'), |
160 |
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'), |
|
161 |
A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), |
|
162 |
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0, |
|
163 |
definition='Failure count before logging a warning to ' |
|
164 |
'authentic2.user_login_failure. No warning will be send if value is ' |
|
165 |
'0.'), |
|
166 |
PUSH_PROFILE_UPDATES=Setting(default=False, definition='Push profile update to linked services'), |
|
167 |
TEMPLATE_VARS=Setting(default={}, definition='Variable to pass to templates'), |
|
168 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR=Setting(default=1.8, |
|
169 |
definition='exponential backoff factor duration as seconds until ' |
|
170 |
'next try after a login failure'), |
|
171 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION=Setting(default=0, |
|
172 |
definition='exponential backoff base factor duration as secondss ' |
|
173 |
'until next try after a login failure'), |
|
174 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION=Setting(default=3600, |
|
175 |
definition='maximum exponential backoff maximum duration as seconds until ' |
|
176 |
'next try after a login failure'), |
|
177 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION=Setting(default=10, |
|
178 |
definition='minimum exponential backoff maximum duration as seconds until ' |
|
179 |
'next try after a login failure'), |
|
180 |
A2_VERIFY_SSL=Setting(default=True, definition='Verify SSL certificate in HTTP requests'), |
|
181 |
A2_ATTRIBUTE_KIND_TITLE_CHOICES=Setting(default=(), definition='Choices for the title attribute kind'), |
|
182 |
A2_CORS_WHITELIST=Setting(default=(), definition='List of origin URL to whitelist, must be scheme://netloc[:port]'), |
|
183 |
A2_EMAIL_CHANGE_TOKEN_LIFETIME=Setting(default=7200, definition='Lifetime in seconds of the ' |
|
184 |
'token sent to verify email adresses'), |
|
236 |
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting( |
|
237 |
default=False, |
|
238 |
definition='Show last character in password fields'), |
|
239 |
A2_AUTH_PASSWORD_ENABLE=Setting( |
|
240 |
default=True, |
|
241 |
definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), |
|
242 |
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting( |
|
243 |
default=0, |
|
244 |
definition='Failure count before logging a warning to ' |
|
245 |
'authentic2.user_login_failure. No warning will be send if value is ' |
|
246 |
'0.'), |
|
247 |
PUSH_PROFILE_UPDATES=Setting( |
|
248 |
default=False, |
|
249 |
definition='Push profile update to linked services'), |
|
250 |
TEMPLATE_VARS=Setting( |
|
251 |
default={}, |
|
252 |
definition='Variable to pass to templates'), |
|
253 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR=Setting( |
|
254 |
default=1.8, |
|
255 |
definition='exponential backoff factor duration as seconds until ' |
|
256 |
'next try after a login failure'), |
|
257 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION=Setting( |
|
258 |
default=0, |
|
259 |
definition='exponential backoff base factor duration as secondss ' |
|
260 |
'until next try after a login failure'), |
|
261 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MAX_DURATION=Setting( |
|
262 |
default=3600, |
|
263 |
definition='maximum exponential backoff maximum duration as seconds until ' |
|
264 |
'next try after a login failure'), |
|
265 |
A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION=Setting( |
|
266 |
default=10, |
|
267 |
definition='minimum exponential backoff maximum duration as seconds until ' |
|
268 |
'next try after a login failure'), |
|
269 |
A2_VERIFY_SSL=Setting( |
|
270 |
default=True, |
|
271 |
definition='Verify SSL certificate in HTTP requests'), |
|
272 |
A2_ATTRIBUTE_KIND_TITLE_CHOICES=Setting( |
|
273 |
default=(), |
|
274 |
definition='Choices for the title attribute kind'), |
|
275 |
A2_CORS_WHITELIST=Setting( |
|
276 |
default=(), |
|
277 |
definition='List of origin URL to whitelist, must be scheme://netloc[:port]'), |
|
278 |
A2_EMAIL_CHANGE_TOKEN_LIFETIME=Setting( |
|
279 |
default=7200, |
|
280 |
definition='Lifetime in seconds of the token sent to verify email adresses'), |
|
185 | 281 |
A2_REDIRECT_WHITELIST=Setting( |
186 | 282 |
default=(), |
187 | 283 |
definition='List of origins which are authorized to ask for redirection.'), |
... | ... | |
199 | 295 |
A2_USER_REMEMBER_ME=Setting( |
200 | 296 |
default=None, |
201 | 297 |
definition='Session duration as seconds when using the remember me ' |
202 |
'checkbox. Truthiness activates the checkbox.'),
|
|
298 |
'checkbox. Truthiness activates the checkbox.'), |
|
203 | 299 |
A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE=Setting( |
204 | 300 |
default=False, |
205 | 301 |
definition='Redirect authenticated users to homepage'), |
206 | 302 |
A2_SET_RANDOM_PASSWORD_ON_RESET=Setting( |
207 | 303 |
default=True, |
208 | 304 |
definition='Set a random password on request to reset the password from the front-office'), |
209 |
A2_ACCOUNTS_URL=Setting(default=None, definition='IdP has no account page, redirect to this one.'), |
|
210 |
A2_CACHE_ENABLED=Setting(default=True, definition='Disable all cache decorators for testing purpose.'), |
|
211 |
A2_ACCEPT_EMAIL_AUTHENTICATION=Setting(default=True, definition='Enable authentication by email'), |
|
212 | ||
305 |
A2_ACCOUNTS_URL=Setting( |
|
306 |
default=None, |
|
307 |
definition='IdP has no account page, redirect to this one.'), |
|
308 |
A2_CACHE_ENABLED=Setting( |
|
309 |
default=True, |
|
310 |
definition='Disable all cache decorators for testing purpose.'), |
|
311 |
A2_ACCEPT_EMAIL_AUTHENTICATION=Setting( |
|
312 |
default=True, |
|
313 |
definition='Enable authentication by email'), |
|
213 | 314 |
) |
214 | 315 | |
215 | 316 |
app_settings = AppSettings(default_settings) |
src/authentic2/apps.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
1 | 16 |
import re |
2 | 17 | |
3 | 18 |
from django.apps import AppConfig |
... | ... | |
24 | 39 |
else: |
25 | 40 |
expected_type = 'TEXT' |
26 | 41 | |
27 | ||
28 | 42 |
def convert_column_to_json(model, column_name): |
29 | 43 |
table_name = model._meta.db_table |
30 | 44 |
src/authentic2/attribute_kinds.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import re |
2 | 18 |
import string |
3 | 19 |
import datetime |
4 |
import io |
|
5 | 20 |
import hashlib |
6 | 21 |
import os |
7 | 22 | |
... | ... | |
15 | 30 |
from django.utils.functional import allow_lazy |
16 | 31 |
from django.utils import html |
17 | 32 |
from django.template.defaultfilters import capfirst |
18 |
from django.core.files import File |
|
19 | 33 |
from django.core.files.storage import default_storage |
20 | 34 | |
21 | 35 |
from rest_framework import serializers |
... | ... | |
65 | 79 |
def get_title_choices(): |
66 | 80 |
return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES |
67 | 81 | |
68 |
validate_phone_number = RegexValidator('^\+?\d{,20}$', message=_('Phone number can start with a + ' |
|
69 |
'an must contain only digits.')) |
|
82 |
validate_phone_number = RegexValidator( |
|
83 |
r'^\+?\d{,20}$', |
|
84 |
message=_('Phone number can start with a + an must contain only digits.')) |
|
70 | 85 | |
71 | 86 | |
72 | 87 |
class PhoneNumberField(forms.CharField): |
... | ... | |
77 | 92 | |
78 | 93 |
def clean(self, value): |
79 | 94 |
if value not in self.empty_values: |
80 |
value = re.sub('[-.\s]', '', value) |
|
95 |
value = re.sub(r'[-.\s]', '', value)
|
|
81 | 96 |
validate_phone_number(value) |
82 | 97 |
return value |
83 | 98 | |
... | ... | |
87 | 102 | |
88 | 103 | |
89 | 104 |
validate_fr_postcode = RegexValidator( |
90 |
'^\d{5}$', message=_('The value must be a valid french postcode')) |
|
105 |
r'^\d{5}$', |
|
106 |
message=_('The value must be a valid french postcode')) |
|
91 | 107 | |
92 | 108 | |
93 | 109 |
class FrPostcodeField(forms.CharField): |
... | ... | |
253 | 269 | |
254 | 270 | |
255 | 271 |
def validate_lun(value): |
256 |
l = [(int(x) * (1 + i % 2)) for i, x in enumerate(reversed(value))] |
|
272 |
l = [(int(x) * (1 + i % 2)) for i, x in enumerate(reversed(value))] # noqa: E741
|
|
257 | 273 |
return sum(x - 9 if x > 10 else x for x in l) % 10 == 0 |
258 | 274 | |
259 | 275 |
src/authentic2/attributes_ng/engine.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 |
from django.core.exceptions import ImproperlyConfigured |
|
4 | 19 |
from django.utils.translation import ugettext as _ |
5 | 20 | |
6 | 21 |
from ..decorators import to_iter, to_list |
... | ... | |
22 | 37 |
def __str__(self): |
23 | 38 |
return 'UnsortableError: %r' % self.unsortable_instances |
24 | 39 | |
40 | ||
25 | 41 |
def topological_sort(source_and_instances, ctx, raise_on_unsortable=False): |
26 | 42 |
''' |
27 | 43 |
Sort instances topologically based on their dependency declarations. |
... | ... | |
40 | 56 |
else: |
41 | 57 |
new_unsorted.append((source, instance)) |
42 | 58 |
unsorted = new_unsorted |
43 |
if len(sorted_list) == len(source_and_instances): # finished ! |
|
59 |
if len(sorted_list) == len(source_and_instances): # finished !
|
|
44 | 60 |
break |
45 |
elif count_sorted == len(sorted_list): # no progress ! |
|
61 |
elif count_sorted == len(sorted_list): # no progress !
|
|
46 | 62 |
if raise_on_unsortable: |
47 | 63 |
raise UnsortableError(sorted_list, unsorted) |
48 | 64 |
else: |
... | ... | |
50 | 66 |
for source, instance in unsorted: |
51 | 67 |
dependencies = set(source.get_dependencies(instance, ctx)) |
52 | 68 |
sorted_list.append((source, instance)) |
53 |
logger.debug('missing dependencies for instance %r of %r: %s', |
|
54 |
instance, source, |
|
55 |
list(dependencies-variables)) |
|
69 |
logger.debug('missing dependencies for instance %r of %r: %s', instance, source, |
|
70 |
list(dependencies - variables)) |
|
56 | 71 |
break |
57 | 72 |
return sorted_list |
58 | 73 | |
74 | ||
59 | 75 |
@to_list |
60 | 76 |
def get_sources(): |
61 | 77 |
''' |
... | ... | |
68 | 84 |
for path in plugin.get_attribute_backends(): |
69 | 85 |
yield utils.import_module_or_class(path) |
70 | 86 | |
87 | ||
71 | 88 |
@to_list |
72 | 89 |
def get_attribute_names(ctx): |
73 | 90 |
''' |
... | ... | |
88 | 105 |
''' |
89 | 106 |
source_and_instances = [] |
90 | 107 |
for source in get_sources(): |
91 |
source_and_instances.extend(((source, instance) for instance in |
|
92 |
source.get_instances(ctx))) |
|
108 |
source_and_instances.extend(((source, instance) for instance in source.get_instances(ctx))) |
|
93 | 109 |
source_and_instances = topological_sort(source_and_instances, ctx) |
94 | 110 |
ctx = ctx.copy() |
95 | 111 |
for source, instance in source_and_instances: |
src/authentic2/attributes_ng/sources/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import abc |
2 | 18 | |
3 | 19 |
from django.utils import six |
src/authentic2/attributes_ng/sources/computed_targeted_id.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
''' |
2 | 18 |
Compute a targeted id based on a hash of existing attributes, to compute a |
3 | 19 |
targetd id for a service provider and a user coming from an LDAP store using |
... | ... | |
24 | 40 | |
25 | 41 |
REQUIRED_KEYS = set(('name', 'source_attributes', 'salt')) |
26 | 42 | |
27 |
UNEXPECTED_KEYS_ERROR = \ |
|
28 |
'{0}: unexpected key(s) {1} in configuration' |
|
29 |
MISSING_KEYS_ERROR = \ |
|
30 |
'{0}: missing key(s) {1} in configuration' |
|
31 |
BAD_CONFIG_ERROR = \ |
|
32 |
'{0}: template attribute source must contain a name, a list of dependencies and a function' |
|
33 |
NOT_CALLABLE_ERROR = \ |
|
34 |
'{0}: function attribute must be callable' |
|
43 |
UNEXPECTED_KEYS_ERROR = '{0}: unexpected key(s) {1} in configuration' |
|
44 |
MISSING_KEYS_ERROR = '{0}: missing key(s) {1} in configuration' |
|
45 |
BAD_CONFIG_ERROR = '{0}: template attribute source must contain a name, a list of dependencies and a function' |
|
46 |
NOT_CALLABLE_ERROR = '{0}: function attribute must be callable' |
|
35 | 47 |
SOURCE_ATTRIBUTE_TYPE_ERROR = '{0}: source_attributes must be a list of string' |
36 | 48 | |
49 | ||
37 | 50 |
def config_error(fmt, *args): |
38 | 51 |
raise ImproperlyConfigured(fmt.format(__name__, *args)) |
39 | 52 | |
53 | ||
40 | 54 |
@to_list |
41 | 55 |
def get_instances(ctx): |
42 | 56 |
''' |
... | ... | |
64 | 78 |
name = instance['name'] |
65 | 79 |
return ((name, instance.get('label', name)),) |
66 | 80 | |
81 | ||
67 | 82 |
def get_dependencies(instance, ctx): |
68 | 83 |
return instance['source_attributes'] |
69 | 84 | |
85 | ||
70 | 86 |
def get_attributes(instance, ctx): |
71 | 87 |
source_attributes = instance['source_attributes'] |
72 | 88 |
source_attributes_values = [] |
src/authentic2/attributes_ng/sources/django_user.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.contrib.auth import get_user_model |
|
1 | 18 |
from django.utils import six |
2 | 19 |
from django.utils.translation import ugettext_lazy as _ |
3 | 20 | |
... | ... | |
6 | 23 |
from ...models import Attribute, AttributeValue |
7 | 24 | |
8 | 25 |
from ...decorators import to_list |
9 |
from ...compat import get_user_model |
|
10 | 26 | |
11 | 27 | |
12 | 28 |
@to_list |
src/authentic2/attributes_ng/sources/format.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import six |
2 | 18 | |
3 | 19 |
from django.core.exceptions import ImproperlyConfigured |
... | ... | |
6 | 22 | |
7 | 23 |
AUTHORIZED_KEYS = set(('name', 'label', 'template')) |
8 | 24 | |
25 | ||
9 | 26 |
@to_list |
10 | 27 |
def get_field_refs(format_string): |
11 | 28 |
''' |
12 | 29 |
Extract the base references from format_string |
13 | 30 |
''' |
14 | 31 |
from string import Formatter |
15 |
l = Formatter().parse(format_string) |
|
32 |
l = Formatter().parse(format_string) # noqa: E741
|
|
16 | 33 |
for p in l: |
17 | 34 |
field_ref = p[1].split('[', 1)[0] |
18 | 35 |
field_ref = field_ref.split('.', 1)[0] |
19 | 36 |
yield field_ref |
20 | 37 | |
21 |
UNEXPECTED_KEYS_ERROR = \ |
|
22 |
'{0}: unexpected ' 'key(s) {1} in configuration' |
|
23 |
FORMAT_STRING_ERROR = \ |
|
24 |
'{0}: template string must contain only keyword references: {1}' |
|
25 |
BAD_CONFIG_ERROR = \ |
|
26 |
'template attribute source must contain a name and at least a template' |
|
27 |
TYPE_ERROR = \ |
|
28 |
'template attribute must be a string' |
|
38 |
UNEXPECTED_KEYS_ERROR = '{0}: unexpected ' 'key(s) {1} in configuration' |
|
39 |
FORMAT_STRING_ERROR = '{0}: template string must contain only keyword references: {1}' |
|
40 |
BAD_CONFIG_ERROR = 'template attribute source must contain a name and at least a template' |
|
41 |
TYPE_ERROR = 'template attribute must be a string' |
|
42 | ||
29 | 43 | |
30 | 44 |
def config_error(fmt, *args): |
31 | 45 |
raise ImproperlyConfigured(fmt.format(__name__, *args)) |
32 | 46 | |
47 | ||
33 | 48 |
@to_list |
34 | 49 |
def get_instances(ctx): |
35 | 50 |
''' |
... | ... | |
54 | 69 |
name = instance['name'] |
55 | 70 |
return ((name, instance.get('label', name)),) |
56 | 71 | |
72 | ||
57 | 73 |
def get_dependencies(instance, ctx): |
58 | 74 |
return get_field_refs(instance['template']) |
59 | 75 | |
76 | ||
60 | 77 |
def get_attributes(instance, ctx): |
61 | 78 |
return {instance['name']: instance['template'].format(**ctx)} |
src/authentic2/attributes_ng/sources/function.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.core.exceptions import ImproperlyConfigured |
2 | 18 | |
3 | 19 |
from ...decorators import to_list |
... | ... | |
6 | 22 | |
7 | 23 |
REQUIRED_KEYS = set(('name', 'dependencies', 'function')) |
8 | 24 | |
9 |
UNEXPECTED_KEYS_ERROR = \ |
|
10 |
'{0}: unexpected key(s) {1} in configuration' |
|
11 |
MISSING_KEYS_ERROR = \ |
|
12 |
'{0}: missing key(s) {1} in configuration' |
|
13 |
BAD_CONFIG_ERROR = \ |
|
14 |
'{0}: template attribute source must contain a name, a list of dependencies and a function' |
|
15 |
NOT_CALLABLE_ERROR = \ |
|
16 |
'{0}: function attribute must be callable' |
|
25 |
UNEXPECTED_KEYS_ERROR = '{0}: unexpected key(s) {1} in configuration' |
|
26 |
MISSING_KEYS_ERROR = '{0}: missing key(s) {1} in configuration' |
|
27 |
BAD_CONFIG_ERROR = '{0}: template attribute source must contain a name, a list of dependencies and a function' |
|
28 |
NOT_CALLABLE_ERROR = '{0}: function attribute must be callable' |
|
17 | 29 |
DEPENDENCY_TYPE_ERROR = '{0}: dependencies must be a list of string' |
18 | 30 | |
31 | ||
19 | 32 |
def config_error(fmt, *args): |
20 | 33 |
raise ImproperlyConfigured(fmt.format(__name__, *args)) |
21 | 34 | |
35 | ||
22 | 36 |
@to_list |
23 | 37 |
def get_instances(ctx): |
24 | 38 |
''' |
... | ... | |
40 | 54 |
not all(map(lambda x: isinstance(x, str), dependencies)): |
41 | 55 |
config_error(DEPENDENCY_TYPE_ERROR) |
42 | 56 | |
43 | ||
44 | 57 |
if not callable(d['function']): |
45 | 58 |
config_error(NOT_CALLABLE_ERROR) |
46 | 59 |
yield d |
... | ... | |
50 | 63 |
name = instance['name'] |
51 | 64 |
return ((name, instance.get('label', name)),) |
52 | 65 | |
66 | ||
53 | 67 |
def get_dependencies(instance, ctx): |
54 | 68 |
return instance.get('dependencies', ()) |
55 | 69 | |
70 | ||
56 | 71 |
def get_attributes(instance, ctx): |
57 | 72 |
args = instance.get('args', ()) |
58 | 73 |
kwargs = instance.get('kwargs', {}) |
src/authentic2/attributes_ng/sources/ldap.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from ...decorators import to_list |
2 | 18 | |
3 | 19 |
from authentic2.backends.ldap_backend import LDAPBackend, LDAPUser |
4 | 20 | |
21 | ||
5 | 22 |
@to_list |
6 | 23 |
def get_instances(ctx): |
7 | 24 |
''' |
... | ... | |
9 | 26 |
''' |
10 | 27 |
return [None] |
11 | 28 | |
29 | ||
12 | 30 |
def get_attribute_names(instance, ctx): |
13 | 31 |
return LDAPBackend.get_attribute_names() |
14 | 32 | |
33 | ||
15 | 34 |
def get_dependencies(instance, ctx): |
16 | 35 |
return ('user',) |
17 | 36 | |
37 | ||
18 | 38 |
def get_attributes(instance, ctx): |
19 | 39 |
user = ctx.get('user') |
20 | 40 |
if user and isinstance(user, LDAPUser): |
src/authentic2/attributes_ng/sources/service_roles.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import ugettext_lazy as _ |
2 | 18 | |
3 | 19 |
from ...models import Service |
src/authentic2/auth2_auth/auth2_ssl/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 | ||
1 | 18 |
class Plugin(object): |
2 | 19 |
def get_before_urls(self): |
3 | 20 |
from . import app_settings |
... | ... | |
5 | 22 |
from authentic2.decorators import setting_enabled, required |
6 | 23 | |
7 | 24 |
return required( |
8 |
setting_enabled('ENABLE', settings=app_settings), |
|
9 |
[ |
|
10 |
url(r'^accounts/sslauth/', include(__name__ + '.urls'))]) |
|
25 |
setting_enabled('ENABLE', settings=app_settings), |
|
26 |
[url(r'^accounts/sslauth/', include(__name__ + '.urls'))]) |
|
11 | 27 | |
12 | 28 |
def get_apps(self): |
13 | 29 |
return [__name__] |
src/authentic2/auth2_auth/auth2_ssl/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib import admin |
2 | 18 | |
3 | 19 |
from . import models |
4 | 20 | |
21 | ||
5 | 22 |
class ClientCertificateAdmin(admin.ModelAdmin): |
6 | 23 |
list_display = ('user', 'subject_dn', 'issuer_dn', 'serial') |
7 | 24 |
src/authentic2/auth2_auth/auth2_ssl/app_settings.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
3 | 17 |
import sys |
4 | 18 | |
... | ... | |
6 | 20 |
class AppSettings(object): |
7 | 21 |
'''Thanks django-allauth''' |
8 | 22 |
__DEFAULTS = dict( |
9 |
# settings for TEST only, make it easy to simulate the SSL
|
|
10 |
# environment
|
|
11 |
ENABLE=False,
|
|
12 |
FORCE_ENV={},
|
|
13 |
ACCEPT_SELF_SIGNED=False,
|
|
14 |
STRICT_MATCH=False,
|
|
15 |
SUBJECT_MATCH_KEYS=('subject_dn', 'issuer_dn'),
|
|
16 |
CREATE_USERNAME_CALLBACK=None,
|
|
17 |
USE_COOKIE=False,
|
|
18 |
CREATE_USER=False,
|
|
23 |
# settings for TEST only, make it easy to simulate the SSL |
|
24 |
# environment |
|
25 |
ENABLE=False, |
|
26 |
FORCE_ENV={}, |
|
27 |
ACCEPT_SELF_SIGNED=False, |
|
28 |
STRICT_MATCH=False, |
|
29 |
SUBJECT_MATCH_KEYS=('subject_dn', 'issuer_dn'), |
|
30 |
CREATE_USERNAME_CALLBACK=None, |
|
31 |
USE_COOKIE=False, |
|
32 |
CREATE_USER=False, |
|
19 | 33 |
) |
20 | 34 | |
21 | 35 |
def __init__(self, prefix): |
... | ... | |
23 | 37 | |
24 | 38 |
def _setting(self, name, dflt): |
25 | 39 |
from django.conf import settings |
26 |
return getattr(settings, self.prefix+name, dflt)
|
|
40 |
return getattr(settings, self.prefix + name, dflt)
|
|
27 | 41 | |
28 | 42 |
def __getattr__(self, name): |
29 | 43 |
if name not in self.__DEFAULTS: |
src/authentic2/auth2_auth/auth2_ssl/authenticators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import ugettext_lazy as _ |
2 | 18 |
import django.forms |
3 | 19 |
src/authentic2/auth2_auth/auth2_ssl/backends.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.contrib.auth import get_user_model |
|
1 | 18 |
from django.db.models import Q |
2 | 19 |
import logging |
3 | 20 | |
4 |
from authentic2.compat import get_user_model |
|
5 | 21 |
from authentic2.backends import is_user_authenticable |
6 | 22 | |
7 | 23 |
from . import models, app_settings |
8 | 24 | |
9 | 25 |
logger = logging.getLogger(__name__) |
10 | 26 | |
27 |
User = get_user_model() |
|
28 | ||
11 | 29 | |
12 | 30 |
class AuthenticationError(Exception): |
13 | 31 |
pass |
... | ... | |
39 | 57 |
simply return the user object. That way, we only need top look-up the |
40 | 58 |
certificate once, when loggin in |
41 | 59 |
""" |
42 |
User = get_user_model() |
|
43 | 60 |
try: |
44 | 61 |
return User.objects.get(id=user_id) |
45 | 62 |
except User.DoesNotExist: |
... | ... | |
80 | 97 |
just a subject for the ClientCertificate. |
81 | 98 |
""" |
82 | 99 |
# auto creation only created a DN for the subject, not the issuer |
83 |
User = get_user_model() |
|
84 | 100 | |
85 | 101 |
# get username and check if the user exists already |
86 | 102 |
if app_settings.CREATE_USERNAME_CALLBACK: |
src/authentic2/auth2_auth/auth2_ssl/middleware.py | ||
---|---|---|
1 |
from django.contrib.auth import authenticate, login |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
17 |
from django.contrib.auth import authenticate, login |
|
3 | 18 | |
4 | 19 |
from . import util, app_settings |
5 | 20 | |
... | ... | |
11 | 26 |
def process_request(self, request): |
12 | 27 |
if app_settings.USE_COOKIE and request.user.is_authenticated(): |
13 | 28 |
return |
14 |
ssl_info = util.SSLInfo(request)
|
|
29 |
ssl_info = util.SSLInfo(request) |
|
15 | 30 |
user = authenticate(ssl_info=ssl_info) |
16 | 31 |
if user and request.user != user: |
17 | 32 |
login(request, user) |
src/authentic2/auth2_auth/auth2_ssl/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db import models |
2 | 18 |
from django.conf import settings |
3 | 19 |
from django.utils import six |
4 | 20 | |
5 | 21 |
from . import util |
6 | 22 | |
23 | ||
7 | 24 |
@six.python_2_unicode_compatible |
8 | 25 |
class ClientCertificate(models.Model): |
9 | 26 |
serial = models.CharField(max_length=255, blank=True) |
src/authentic2/auth2_auth/auth2_ssl/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 |
from .views import (handle_request, post_account_linking, delete_certificate, |
|
3 |
error_ssl) |
|
18 |
from .views import (handle_request, post_account_linking, delete_certificate, error_ssl) |
|
4 | 19 | |
5 | 20 |
urlpatterns = [ |
6 | 21 |
url(r'^$', |
src/authentic2/auth2_auth/auth2_ssl/util.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import base64 |
2 | 18 |
import six |
3 | 19 | |
... | ... | |
11 | 27 |
'verify': 'SSL_CLIENT_VERIFY', |
12 | 28 |
} |
13 | 29 | |
30 | ||
14 | 31 |
def normalize_cert(certificate_pem): |
15 | 32 |
'''Normalize content of the certificate''' |
16 | 33 |
base64_content = ''.join(certificate_pem.splitlines()[1:-1]) |
17 | 34 |
content = base64.b64decode(base64_content) |
18 | 35 |
return base64.b64encode(content) |
19 | 36 | |
37 | ||
20 | 38 |
def explode_dn(dn): |
21 | 39 |
'''Extract sub element of a DN as displayed by mod_ssl or nginx_ssl''' |
22 | 40 |
dn = dn.strip('/') |
23 | 41 |
parts = dn.split('/') |
24 | 42 |
parts = [part.split('=') for part in parts] |
25 |
parts = [(part[0], part[1].decode('string_escape').decode('utf-8')) |
|
26 |
for part in parts] |
|
43 |
parts = [(part[0], part[1].decode('string_escape').decode('utf-8')) for part in parts] |
|
27 | 44 |
return parts |
28 | 45 | |
46 | ||
29 | 47 |
TRANSFORM = { |
30 |
'cert': normalize_cert,
|
|
48 |
'cert': normalize_cert, |
|
31 | 49 |
} |
32 | 50 | |
51 | ||
33 | 52 |
class SSLInfo(object): |
34 | 53 |
""" |
35 | 54 |
Encapsulates the SSL environment variables in a read-only object. It |
... | ... | |
48 | 67 |
else: |
49 | 68 |
raise EnvironmentError('The SSL authentication currently only \ |
50 | 69 |
works with mod_python or wsgi requests') |
51 |
self.read_env(env);
|
|
70 |
self.read_env(env) |
|
52 | 71 |
pass |
53 | 72 | |
54 | 73 |
def read_env(self, env): |
... | ... | |
64 | 83 |
else: |
65 | 84 |
self.__dict__[attr] = None |
66 | 85 | |
67 | ||
68 | 86 |
if self.__dict__['verify'] == 'SUCCESS': |
69 | 87 |
self.__dict__['verify'] = True |
70 | 88 |
else: |
src/authentic2/auth2_auth/auth2_ssl/views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 | 19 |
from django.utils.translation import ugettext as _ |
... | ... | |
16 | 32 | |
17 | 33 |
logger = logging.getLogger(__name__) |
18 | 34 | |
35 | ||
19 | 36 |
def handle_request(request): |
20 | 37 |
# Check certificate validity |
21 |
ssl_info = util.SSLInfo(request)
|
|
38 |
ssl_info = util.SSLInfo(request) |
|
22 | 39 |
accept_self_signed = app_settings.ACCEPT_SELF_SIGNED |
23 | 40 | |
24 | 41 |
if not ssl_info.cert: |
25 |
logger.error('SSL Client Authentication failed: ' |
|
26 |
'SSL CGI variable CERT is missing') |
|
42 |
logger.error('SSL Client Authentication failed: SSL CGI variable CERT is missing') |
|
27 | 43 |
messages.add_message(request, messages.ERROR, |
28 |
_('SSL Client Authentication failed. ' |
|
29 |
'No client certificate found.')) |
|
44 |
_('SSL Client Authentication failed. No client certificate found.')) |
|
30 | 45 |
return redirect_to_login(request) |
31 | 46 |
elif not accept_self_signed and not ssl_info.verify: |
32 |
logger.error('SSL Client Authentication failed: ' |
|
33 |
'SSL CGI variable VERIFY is not SUCCESS') |
|
47 |
logger.error('SSL Client Authentication failed: SSL CGI variable VERIFY is not SUCCESS') |
|
34 | 48 |
messages.add_message(request, messages.ERROR, |
35 |
_('SSL Client Authentication failed. ' |
|
36 |
'Your client certificate is not valid.')) |
|
49 |
_('SSL Client Authentication failed. Your client certificate is not valid.')) |
|
37 | 50 |
return redirect_to_login(request) |
38 | 51 | |
39 | 52 |
# SSL entries for this certificate? |
... | ... | |
51 | 64 |
else: |
52 | 65 |
logger.error('account creation failure') |
53 | 66 |
messages.add_message(request, messages.ERROR, |
54 |
_('SSL Client Authentication failed. Internal server error.')) |
|
67 |
_('SSL Client Authentication failed. Internal server error.'))
|
|
55 | 68 |
return redirect_to_login(request) |
56 | 69 | |
57 | 70 |
# No SSL entries and no user session, redirect account linking page |
... | ... | |
61 | 74 |
# No SSL entries but active user session, perform account linking |
62 | 75 |
if not user and request.user.is_authenticated(): |
63 | 76 |
from backend import SSLBackend |
64 |
if SSLBackend().link_user(ssl_info, request.user): |
|
65 |
logger.info('Successful linking of the SSL ' |
|
66 |
'Certificate to an account, redirection to %s' % next_url) |
|
67 |
else: |
|
77 |
if not SSLBackend().link_user(ssl_info, request.user): |
|
68 | 78 |
logger.error('login() failed') |
69 | 79 |
messages.add_message(request, messages.ERROR, |
70 |
_('SSL Client Authentication failed. Internal server error.')) |
|
80 |
_('SSL Client Authentication failed. Internal server error.'))
|
|
71 | 81 |
return redirect_to_login(request) |
82 |
logger.info('Successful linking of the SSL Certificate to an account') |
|
72 | 83 | |
73 | 84 |
# SSL Entries found for this certificate, |
74 | 85 |
# if the user is logged out, we login |
... | ... | |
81 | 92 |
# check that the SSL entry for the certificate is this user. |
82 | 93 |
# else, we make this certificate point on that user. |
83 | 94 |
if user.username != request.user.username: |
84 |
logger.warning(u'The certificate belongs to %s, ' |
|
85 |
'but %s is logged with, we change the association!', |
|
86 |
user, request.user) |
|
95 |
logger.warning(u'The certificate belongs to %s, but %s is logged with, we change the association!', |
|
96 |
user, request.user) |
|
87 | 97 |
from backends import SSLBackend |
88 | 98 |
cert = SSLBackend().get_certificate(ssl_info) |
89 | 99 |
cert.user = request.user |
90 | 100 |
cert.save() |
91 | 101 |
return continue_to_next_url(request) |
92 | 102 | |
93 |
### |
|
94 |
# post_account_linking |
|
95 |
# @request |
|
96 |
# |
|
97 |
# Called after an account linking. |
|
98 |
### |
|
103 | ||
99 | 104 |
@csrf_exempt |
100 | 105 |
def post_account_linking(request): |
101 |
logger.info('auth2_ssl Return after account linking form filled') |
|
102 | 106 |
if request.method == "POST": |
103 |
if 'do_creation' in request.POST \ |
|
104 |
and request.POST['do_creation'] == 'on': |
|
105 |
logger.info('account creation asked') |
|
107 |
if 'do_creation' in request.POST and request.POST['do_creation'] == 'on': |
|
106 | 108 |
request.session['do_creation'] = 'do_creation' |
107 | 109 |
return redirect_to_login(request, login_url='user_signin_ssl') |
108 | 110 |
form = AuthenticationForm(data=request.POST) |
109 | 111 |
if form.is_valid(): |
110 |
logger.info('form valid') |
|
111 | 112 |
user = form.get_user() |
112 |
try: |
|
113 |
login(request, user) |
|
114 |
record_authentication_event(request, how='password') |
|
115 |
except: |
|
116 |
logger.error('login() failed') |
|
117 |
messages.add_message(request, messages.ERROR, |
|
118 |
_('SSL Client Authentication failed. Internal server error.')) |
|
119 | ||
120 |
logger.debug('session opened') |
|
113 |
login(request, user) |
|
114 |
record_authentication_event(request, how='password') |
|
121 | 115 |
return redirect_to_login(request, login_url='user_signin_ssl') |
122 | 116 |
else: |
123 |
logger.warning('form not valid - Try again! (Brute force?)') |
|
124 | 117 |
return render(request, 'auth/account_linking_ssl.html') |
125 | 118 |
else: |
126 | 119 |
return render(request, 'auth/account_linking_ssl.html') |
127 | 120 | |
121 | ||
128 | 122 |
def profile(request, template_name='ssl/profile.html', *args, **kwargs): |
129 | 123 |
context = kwargs.pop('context', {}) |
130 | 124 |
certificates = models.ClientCertificate.objects.filter(user=request.user) |
131 | 125 |
context.update({'certificates': certificates}) |
132 | 126 |
return render_to_string(template_name, context, request=request) |
133 | 127 | |
128 | ||
134 | 129 |
def delete_certificate(request, certificate_pk): |
135 | 130 |
qs = models.ClientCertificate.objects.filter(pk=certificate_pk) |
136 | 131 |
count = qs.count() |
... | ... | |
138 | 133 |
if count: |
139 | 134 |
logger.info('client certificate %s deleted', certificate_pk) |
140 | 135 |
messages.info(request, _('Certificate deleted.')) |
141 |
return redirect(request, 'account_management', |
|
142 |
fragment='a2-ssl-certificate-profile') |
|
136 |
return redirect(request, 'account_management', fragment='a2-ssl-certificate-profile')
|
|
137 | ||
143 | 138 | |
144 | 139 |
class SslErrorView(TemplateView): |
145 | 140 |
template_name = 'error_ssl.html' |
src/authentic2/authentication.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from authentic2_idp_oidc.models import OIDCClient |
2 | 18 | |
3 | 19 |
from rest_framework.exceptions import AuthenticationFailed |
src/authentic2/authenticators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.shortcuts import render |
2 | 18 |
from django.utils.translation import ugettext as _, ugettext_lazy |
3 | 19 | |
4 |
from . import views, app_settings, utils, constants, forms |
|
20 |
from . import views, app_settings, utils, constants |
|
21 |
from .forms import authentication as authentication_forms |
|
5 | 22 | |
6 | 23 | |
7 | 24 |
class LoginPasswordAuthenticator(object): |
... | ... | |
20 | 37 |
context = kwargs.get('context', {}) |
21 | 38 |
is_post = request.method == 'POST' and self.submit_name in request.POST |
22 | 39 |
data = request.POST if is_post else None |
23 |
form = forms.AuthenticationForm(request=request, data=data) |
|
40 |
form = authentication_forms.AuthenticationForm(request=request, data=data)
|
|
24 | 41 |
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION: |
25 | 42 |
form.fields['username'].label = _('Username or email') |
26 | 43 |
if app_settings.A2_USERNAME_LABEL: |
src/authentic2/backends/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib.auth import get_user_model |
2 | 18 |
from authentic2 import app_settings |
3 | 19 | |
... | ... | |
25 | 41 |
return get_user_queryset().filter(pk=user.pk).exists() |
26 | 42 | |
27 | 43 | |
28 |
from .ldap_backend import LDAPBackend |
|
29 |
from .models_backend import ModelBackend |
|
44 |
from .ldap_backend import LDAPBackend # noqa: F401 |
|
45 |
from .models_backend import ModelBackend # noqa: F401 |
src/authentic2/backends/ldap_backend.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
try: |
2 | 18 |
import ldap |
3 | 19 |
import ldap.modlist |
... | ... | |
19 | 35 |
# code originaly copied from by now merely inspired by |
20 | 36 |
# http://www.amherst.k12.oh.us/django-ldap.html |
21 | 37 | |
22 |
log = logging.getLogger(__name__) |
|
23 | ||
24 | 38 |
from django.core.exceptions import ImproperlyConfigured |
25 | 39 |
from django.conf import settings |
40 |
from django.contrib.auth import get_user_model |
|
26 | 41 |
from django.contrib.auth.models import Group |
27 | 42 |
from django.utils.encoding import force_bytes, force_text |
28 | 43 |
from django.utils import six |
29 | 44 |
from django.utils.six.moves.urllib import parse as urlparse |
30 |
from django.utils import six |
|
31 | 45 | |
32 | 46 |
from authentic2.a2_rbac.models import Role |
33 | 47 | |
34 | 48 |
from authentic2.compat_lasso import lasso |
35 | 49 | |
36 | 50 |
from authentic2 import crypto, app_settings |
37 |
from authentic2.decorators import to_list |
|
38 |
from authentic2.compat import get_user_model |
|
39 | 51 |
from authentic2.models import UserExternalId |
40 | 52 |
from authentic2.middleware import StoreRequestMiddleware |
41 | 53 |
from authentic2.user_login_failure import user_login_failure, user_login_success |
... | ... | |
46 | 58 | |
47 | 59 |
from authentic2.backends import is_user_authenticable |
48 | 60 | |
61 |
log = logging.getLogger(__name__) |
|
62 | ||
63 |
User = get_user_model() |
|
49 | 64 | |
50 | 65 |
DEFAULT_CA_BUNDLE = '' |
51 | 66 | |
... | ... | |
212 | 227 |
raise NotImplementedError |
213 | 228 | |
214 | 229 | |
215 |
class LDAPUser(get_user_model()):
|
|
230 |
class LDAPUser(User):
|
|
216 | 231 |
SESSION_LDAP_DATA_KEY = 'ldap-data' |
217 | 232 |
_changed = False |
218 | 233 | |
... | ... | |
261 | 276 |
def update_request(self): |
262 | 277 |
request = StoreRequestMiddleware.get_request() |
263 | 278 |
if request: |
264 |
assert not request.session is None
|
|
279 |
assert request.session is not None
|
|
265 | 280 |
self.init_to_session(request.session) |
266 | 281 | |
267 | 282 |
def init_from_request(self): |
268 | 283 |
request = StoreRequestMiddleware.get_request() |
269 |
assert request and not request.session is None
|
|
284 |
assert request and request.session is not None
|
|
270 | 285 |
self.init_from_session(request.session) |
271 | 286 | |
272 | 287 |
def keep_password(self, password): |
... | ... | |
377 | 392 |
'bindpw': '', |
378 | 393 |
'bindsasl': (), |
379 | 394 |
'user_dn_template': '', |
380 |
'user_filter': 'uid=%s', # will be '(|(mail=%s)(uid=%s))' if A2_ACCEPT_EMAIL_AUTHENTICATION is set (see update_default) |
|
395 |
'user_filter': 'uid=%s', # will be '(|(mail=%s)(uid=%s))' if |
|
396 |
# A2_ACCEPT_EMAIL_AUTHENTICATION is set (see update_default) |
|
381 | 397 |
'sync_ldap_users_filter': '', |
382 | 398 |
'user_basedn': '', |
383 | 399 |
'group_dn_template': '', |
... | ... | |
586 | 602 |
if not block['connect_with_user_credentials']: |
587 | 603 |
try: |
588 | 604 |
self.bind(block, conn) |
589 |
except Exception as e:
|
|
605 |
except Exception: |
|
590 | 606 |
log.exception(u'rebind failure after login bind') |
591 | 607 |
raise ldap.SERVER_DOWN |
592 | 608 |
break |
... | ... | |
739 | 755 |
for role_name in role_names: |
740 | 756 |
role, error = self.get_role(block, role_id=role_name) |
741 | 757 |
if role is None: |
742 |
log.warning('error %s: couldn\'t retrieve role %r', |
|
743 |
error, role_name) |
|
758 |
log.warning('error %s: couldn\'t retrieve role %r', error, role_name) |
|
744 | 759 |
continue |
745 | 760 |
# Add missing roles |
746 | 761 |
if dn in role_dns and role not in roles: |
... | ... | |
842 | 857 |
if group not in groups: |
843 | 858 |
user.groups.add(group) |
844 | 859 | |
845 | ||
846 | 860 |
def populate_mandatory_roles(self, user, block): |
847 | 861 |
mandatory_roles = block.get('set_mandatory_roles') |
848 | 862 |
if not mandatory_roles: |
... | ... | |
854 | 868 |
for role_name in mandatory_roles: |
855 | 869 |
role, error = self.get_role(block, role_id=role_name) |
856 | 870 |
if role is None: |
857 |
log.warning('error %s: couldn\'t retrieve role %r', |
|
858 |
error, role_name) |
|
871 |
log.warning('error %s: couldn\'t retrieve role %r', error, role_name) |
|
859 | 872 |
continue |
860 | 873 |
if role not in roles: |
861 | 874 |
user.roles.add(role) |
... | ... | |
996 | 1009 |
return ' '.join(part for part in parts) |
997 | 1010 | |
998 | 1011 |
def lookup_by_username(self, username): |
999 |
User = get_user_model() |
|
1000 | 1012 |
try: |
1001 | 1013 |
log.debug('lookup using username %r', username) |
1002 | 1014 |
return LDAPUser.objects.prefetch_related('groups').get(username=username) |
... | ... | |
1004 | 1016 |
return |
1005 | 1017 | |
1006 | 1018 |
def lookup_by_external_id(self, block, attributes): |
1007 |
User = get_user_model() |
|
1008 | 1019 |
for eid_tuple in map_text(block['external_id_tuples']): |
1009 | 1020 |
external_id = self.build_external_id(eid_tuple, attributes) |
1010 | 1021 |
if not external_id: |
... | ... | |
1019 | 1030 |
user = users[0] |
1020 | 1031 |
if len(users) > 1: |
1021 | 1032 |
log.info('found %d users, collectings roles into the first one and deleting the other ones.', |
1022 |
len(users))
|
|
1033 |
len(users)) |
|
1023 | 1034 |
for other in users[1:]: |
1024 | 1035 |
for r in other.roles.all(): |
1025 | 1036 |
user.roles.add(r) |
... | ... | |
1312 | 1323 |
if isinstance(cls._DEFAULTS[d], bool) and not isinstance(block[d], bool): |
1313 | 1324 |
raise ImproperlyConfigured( |
1314 | 1325 |
'LDAP_AUTH_SETTINGS: attribute %r must be a boolean' % d) |
1315 |
if (isinstance(cls._DEFAULTS[d], (list, tuple)) and
|
|
1316 |
not isinstance(block[d], (list, tuple))): |
|
1326 |
if (isinstance(cls._DEFAULTS[d], (list, tuple)) |
|
1327 |
and not isinstance(block[d], (list, tuple))):
|
|
1317 | 1328 |
raise ImproperlyConfigured( |
1318 | 1329 |
'LDAP_AUTH_SETTINGS: attribute %r must be a list or a tuple' % d) |
1319 | 1330 |
if isinstance(cls._DEFAULTS[d], dict) and not isinstance(block[d], dict): |
src/authentic2/backends/models_backend.py | ||
---|---|---|
1 |
# |
|
1 |
# authentic2 - versatile identity manager
|
|
2 | 2 |
# Copyright (C) 2010-2019 Entr'ouvert |
3 | 3 |
# |
4 | 4 |
# This program is free software: you can redistribute it and/or modify it |
src/authentic2/cbv.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt |
2 | 18 | |
3 | 19 |
from django.utils.decorators import method_decorator |
src/authentic2/compat.py | ||
---|---|---|
1 |
from datetime import datetime |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
2 | 17 |
import inspect |
3 | 18 | |
4 | 19 |
import django |
5 |
from django.conf import settings |
|
6 | 20 |
from django.db import connection |
7 | 21 |
from django.db.utils import OperationalError |
8 | 22 |
from django.core.exceptions import ImproperlyConfigured |
9 | 23 | |
10 | 24 |
from django.contrib.auth.tokens import PasswordResetTokenGenerator |
11 | 25 | |
12 |
try: |
|
13 |
from django.contrib.auth import get_user_model |
|
14 |
except ImportError: |
|
15 |
from django.contrib.auth.models import User |
|
16 |
get_user_model = lambda: User |
|
17 | ||
18 |
try: |
|
19 |
from django.db.transaction import atomic |
|
20 |
commit_on_success = atomic |
|
21 |
except ImportError: |
|
22 |
from django.db.transaction import commit_on_success |
|
23 | ||
24 |
user_model_label = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') |
|
25 | 26 | |
26 | 27 |
default_token_generator = PasswordResetTokenGenerator() |
27 | 28 |
src/authentic2/compat_lasso.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
try: |
2 | 18 |
import lasso |
3 | 19 |
except ImportError: |
src/authentic2/constants.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 | |
2 | 18 |
NONCE_FIELD_NAME = 'nonce' |
3 | 19 |
CANCEL_FIELD_NAME = 'cancel' |
src/authentic2/context_processors.py | ||
---|---|---|
1 |
from collections import defaultdict |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
3 | 17 |
from pkg_resources import get_distribution |
4 | 18 |
from django.conf import settings |
5 | 19 | |
6 | 20 |
from . import utils, app_settings, constants |
7 | 21 | |
22 | ||
8 | 23 |
class UserFederations(object): |
9 | 24 |
'''Provide access to all federations of the current user''' |
10 | 25 |
def __init__(self, request): |
11 | 26 |
self.request = request |
12 | 27 | |
13 | 28 |
def __getattr__(self, name): |
14 |
d = { 'provider': None, 'links': [] }
|
|
29 |
d = {'provider': None, 'links': [] } |
|
15 | 30 |
if name.startswith('service_'): |
16 | 31 |
try: |
17 | 32 |
provider_id = int(name.split('_', 1)[1]) |
... | ... | |
29 | 44 | |
30 | 45 |
__AUTHENTIC2_DISTRIBUTION = None |
31 | 46 | |
47 | ||
32 | 48 |
def a2_processor(request): |
33 | 49 |
global __AUTHENTIC2_DISTRIBUTION |
34 | 50 |
variables = {} |
src/authentic2/cors.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from .decorators import SessionCache |
2 | 18 | |
3 | 19 |
from django.conf import settings |
src/authentic2/crypto.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import base64 |
2 | 18 |
import hashlib |
3 | 19 |
import struct |
... | ... | |
101 | 117 | |
102 | 118 |
iv = hashmod.new(salt).digest() |
103 | 119 | |
104 |
prf = lambda secret, salt: HMAC.new(secret, salt, hashmod).digest() |
|
120 |
def prf(secret, salt): |
|
121 |
return HMAC.new(secret, salt, hashmod).digest() |
|
105 | 122 | |
106 | 123 |
aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf) |
107 | 124 | |
... | ... | |
122 | 139 |
hashmod = SHA256 |
123 | 140 |
key_size = 16 |
124 | 141 |
hmac_size = key_size |
125 |
prf = lambda secret, salt: HMAC.new(secret, salt, hashmod).digest() |
|
142 | ||
143 |
def prf(secret, salt): |
|
144 |
return HMAC.new(secret, salt, hashmod).digest() |
|
126 | 145 | |
127 | 146 |
try: |
128 | 147 |
try: |
src/authentic2/custom_user/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
default_app_config = 'authentic2.custom_user.apps.CustomUserConfig' |
src/authentic2/custom_user/apps.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db import DEFAULT_DB_ALIAS, router |
2 | 18 |
from django.apps import AppConfig |
3 | 19 |
src/authentic2/custom_user/management/commands/changepassword.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import unicode_literals, print_function |
2 | 18 | |
3 | 19 |
import getpass |
... | ... | |
35 | 51 |
UserModel = get_user_model() |
36 | 52 | |
37 | 53 |
qs = UserModel._default_manager.using(options.get('database')) |
38 |
qs = qs.filter(Q(uuid=username)|Q(username=username)|Q(email=username))
|
|
54 |
qs = qs.filter(Q(uuid=username) | Q(username=username) | Q(email=username))
|
|
39 | 55 |
try: |
40 | 56 |
u = qs.get() |
41 | 57 |
except UserModel.DoesNotExist: |
... | ... | |
44 | 60 |
while True: |
45 | 61 |
print('Select a user:') |
46 | 62 |
for i, user in enumerate(qs): |
47 |
print('%d.' % (i+1), user)
|
|
63 |
print('%d.' % (i + 1), user)
|
|
48 | 64 |
print('> ', end=' ') |
49 | 65 |
try: |
50 | 66 |
j = input() |
51 | 67 |
except SyntaxError: |
52 | 68 |
print('Please enter an integer') |
53 | 69 |
continue |
54 |
if not isinstance(uid, int):
|
|
70 |
if not isinstance(j, int):
|
|
55 | 71 |
print('Please enter an integer') |
56 | 72 |
continue |
57 | 73 |
try: |
58 |
u = qs[j-1]
|
|
74 |
u = qs[j - 1]
|
|
59 | 75 |
break |
60 | 76 |
except IndexError: |
61 | 77 |
print('Please enter an integer between 1 and %d' % qs.count()) |
src/authentic2/custom_user/management/commands/fix-attributes.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import unicode_literals, print_function |
2 | 18 | |
3 | 19 |
from django.core.management.base import BaseCommand |
src/authentic2/custom_user/managers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db import models |
2 | 18 |
from django.utils import timezone |
3 | 19 |
from django.contrib.auth.models import BaseUserManager |
... | ... | |
13 | 29 |
searchable_attributes = Attribute.objects.filter(searchable=True) |
14 | 30 |
queries = [] |
15 | 31 |
for term in terms: |
16 |
q = (models.query.Q(username__icontains=term) | |
|
17 |
models.query.Q(first_name__icontains=term) | |
|
18 |
models.query.Q(last_name__icontains=term) | |
|
19 |
models.query.Q(email__icontains=term)) |
|
32 |
q = ( |
|
33 |
models.query.Q(username__icontains=term) |
|
34 |
| models.query.Q(first_name__icontains=term) |
|
35 |
| models.query.Q(last_name__icontains=term) |
|
36 |
| models.query.Q(email__icontains=term) |
|
37 |
) |
|
20 | 38 |
for a in searchable_attributes: |
21 | 39 |
if a.name in ('first_name', 'last_name'): |
22 | 40 |
continue |
src/authentic2/custom_user/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db import models |
2 | 18 |
from django.utils import timezone |
3 | 19 |
from django.core.mail import send_mail |
... | ... | |
85 | 101 |
def __getattr__(self, name): |
86 | 102 |
v = getattr(self.user.attributes, name, None) |
87 | 103 |
return ( |
88 |
v is not None and
|
|
89 |
v == getattr(self.user.verified_attributes, name, None) |
|
104 |
v is not None |
|
105 |
and v == getattr(self.user.verified_attributes, name, None)
|
|
90 | 106 |
) |
91 | 107 | |
92 | 108 | |
... | ... | |
103 | 119 | |
104 | 120 |
Username, password and email are required. Other fields are optional. |
105 | 121 |
""" |
106 |
uuid = models.CharField(_('uuid'), max_length=32, |
|
107 |
default=utils.get_hex_uuid, editable=False, unique=True) |
|
122 |
uuid = models.CharField( |
|
123 |
_('uuid'), |
|
124 |
max_length=32, |
|
125 |
default=utils.get_hex_uuid, editable=False, unique=True) |
|
108 | 126 |
username = models.CharField(_('username'), max_length=256, null=True, blank=True) |
109 | 127 |
first_name = models.CharField(_('first name'), max_length=128, blank=True) |
110 | 128 |
last_name = models.CharField(_('last name'), max_length=128, blank=True) |
111 |
email = models.EmailField(_('email address'), blank=True, |
|
112 |
validators=[validators.EmailValidator], max_length=254) |
|
129 |
email = models.EmailField( |
|
130 |
_('email address'), |
|
131 |
blank=True, |
|
132 |
validators=[validators.EmailValidator], |
|
133 |
max_length=254) |
|
113 | 134 |
email_verified = models.BooleanField( |
114 | 135 |
default=False, |
115 | 136 |
verbose_name=_('email verified')) |
116 |
is_staff = models.BooleanField(_('staff status'), default=False, |
|
137 |
is_staff = models.BooleanField( |
|
138 |
_('staff status'), |
|
139 |
default=False, |
|
117 | 140 |
help_text=_('Designates whether the user can log into this admin ' |
118 | 141 |
'site.')) |
119 |
is_active = models.BooleanField(_('active'), default=True, |
|
142 |
is_active = models.BooleanField( |
|
143 |
_('active'), |
|
144 |
default=True, |
|
120 | 145 |
help_text=_('Designates whether this user should be treated as ' |
121 | 146 |
'active. Unselect this instead of deleting accounts.')) |
122 | 147 |
date_joined = models.DateTimeField(_('date joined'), default=timezone.now) |
src/authentic2/data_transfer.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib.contenttypes.models import ContentType |
2 | 18 | |
3 | 19 |
from django_rbac.models import Operation |
src/authentic2/decorators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import base64 |
2 | 18 |
import pickle |
3 | 19 |
import re |
... | ... | |
6 | 22 |
import time |
7 | 23 |
from functools import wraps |
8 | 24 | |
9 |
from django.contrib.auth.decorators import login_required |
|
10 | 25 |
from django.views.debug import technical_404_response |
11 | 26 |
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest |
12 | 27 |
from django.core.cache import cache as django_cache |
13 | 28 |
from django.core.exceptions import ValidationError |
14 | 29 |
from django.utils import six |
15 | 30 | |
16 |
from . import utils, app_settings, middleware |
|
17 |
from .utils import to_list, to_iter |
|
31 |
from . import app_settings, middleware |
|
32 |
# XXX: import to_list for retrocompaibility |
|
33 |
from .utils import to_list, to_iter # noqa: F401 |
|
18 | 34 | |
19 | 35 | |
20 | 36 |
class CacheUnusable(RuntimeError): |
... | ... | |
32 | 48 |
return f |
33 | 49 |
return decorator |
34 | 50 | |
51 | ||
35 | 52 |
def setting_enabled(name, settings=app_settings): |
36 | 53 |
'''Generate a decorator for enabling a view based on a setting''' |
37 | 54 |
full_name = getattr(settings, 'prefix', '') + name |
55 | ||
38 | 56 |
def test(): |
39 | 57 |
return getattr(settings, name, False) |
40 | 58 |
return unless(test, 'please enable %s' % full_name) |
41 | 59 | |
60 | ||
42 | 61 |
def lasso_required(): |
43 | 62 |
def test(): |
44 | 63 |
try: |
45 |
import lasso |
|
64 |
import lasso # noqa: F401
|
|
46 | 65 |
return True |
47 | 66 |
except ImportError: |
48 | 67 |
return False |
49 | 68 |
return unless(test, 'please install lasso') |
50 | 69 | |
51 |
def required(wrapping_functions,patterns_rslt): |
|
70 | ||
71 |
def required(wrapping_functions, patterns_rslt): |
|
52 | 72 |
''' |
53 | 73 |
Used to require 1..n decorators in any view returned by a url tree |
54 | 74 | |
... | ... | |
69 | 89 |
patterns(...) |
70 | 90 |
) |
71 | 91 |
''' |
72 |
if not hasattr(wrapping_functions,'__iter__'):
|
|
92 |
if not hasattr(wrapping_functions, '__iter__'):
|
|
73 | 93 |
wrapping_functions = (wrapping_functions,) |
74 | 94 | |
75 | 95 |
return [ |
76 |
_wrap_instance__resolve(wrapping_functions,instance) |
|
96 |
_wrap_instance__resolve(wrapping_functions, instance)
|
|
77 | 97 |
for instance in patterns_rslt |
78 | 98 |
] |
79 | 99 | |
80 |
def _wrap_instance__resolve(wrapping_functions,instance): |
|
81 |
if not hasattr(instance,'resolve'): return instance |
|
82 |
resolve = getattr(instance,'resolve') |
|
83 | 100 | |
84 |
def _wrap_func_in_returned_resolver_match(*args,**kwargs): |
|
85 |
rslt = resolve(*args,**kwargs) |
|
101 |
def _wrap_instance__resolve(wrapping_functions, instance): |
|
102 |
if not hasattr(instance, 'resolve'): |
|
103 |
return instance |
|
104 |
resolve = getattr(instance, 'resolve') |
|
86 | 105 | |
87 |
if not hasattr(rslt,'func'):return rslt |
|
88 |
f = getattr(rslt,'func') |
|
106 |
def _wrap_func_in_returned_resolver_match(*args, **kwargs): |
|
107 |
rslt = resolve(*args, **kwargs) |
|
108 | ||
109 |
if not hasattr(rslt, 'func'): |
|
110 |
return rslt |
|
111 |
f = getattr(rslt, 'func') |
|
89 | 112 | |
90 | 113 |
for _f in reversed(wrapping_functions): |
91 | 114 |
# @decorate the function from inner to outter |
92 | 115 |
f = _f(f) |
93 | 116 | |
94 |
setattr(rslt,'func',f)
|
|
117 |
setattr(rslt, 'func', f)
|
|
95 | 118 | |
96 | 119 |
return rslt |
97 | 120 | |
98 |
setattr(instance,'resolve',_wrap_func_in_returned_resolver_match) |
|
99 | ||
121 |
setattr(instance, 'resolve', _wrap_func_in_returned_resolver_match) |
|
100 | 122 |
return instance |
101 | 123 | |
124 | ||
102 | 125 |
class CacheDecoratorBase(object): |
103 | 126 |
'''Base class to build cache decorators. |
104 | 127 | |
... | ... | |
106 | 129 |
''' |
107 | 130 |
def __new__(cls, *args, **kwargs): |
108 | 131 |
if len(args) > 1: |
109 |
raise TypeError('%s got unexpected arguments, only one argument '
|
|
110 |
'must be given, the function to decorate' % cls.__name__)
|
|
132 |
raise TypeError( |
|
133 |
'%s got unexpected arguments, only one argument must be given, the function to decorate' % cls.__name__)
|
|
111 | 134 |
if args: |
112 | 135 |
# Case of a decorator used directly |
113 | 136 |
return cls(**kwargs)(args[0]) |
... | ... | |
139 | 162 |
key = self.key(*args, **kwargs) |
140 | 163 |
value, tstamp = self.get(key) |
141 | 164 |
if tstamp is not None: |
142 |
if self.timeout is None or \
|
|
143 |
tstamp + self.timeout > now:
|
|
144 |
return value
|
|
165 |
if (self.timeout is None
|
|
166 |
or tstamp + self.timeout > now):
|
|
167 |
return value |
|
145 | 168 |
if hasattr(self, 'delete'): |
146 | 169 |
self.delete(key, (key, tstamp)) |
147 | 170 |
value = func(*args, **kwargs) |
148 | 171 |
self.set(key, (value, now)) |
149 | 172 |
return value |
150 |
except CacheUnusable: # fallback when cache cannot be used |
|
173 |
except CacheUnusable: # fallback when cache cannot be used
|
|
151 | 174 |
return func(*args, **kwargs) |
152 | 175 |
f.cache = self |
153 | 176 |
return f |
154 | 177 | |
155 | 178 |
def key(self, *args, **kwargs): |
156 | 179 |
'''Transform arguments to string and build a key from it''' |
157 |
parts = [str(id(self))] # add cache instance to the key |
|
180 |
parts = [str(id(self))] # add cache instance to the key
|
|
158 | 181 |
if self.hostname_vary: |
159 | 182 |
request = middleware.StoreRequestMiddleware.get_request() |
160 | 183 |
if request: |
161 | 184 |
parts.append(request.get_host()) |
162 |
else:
|
|
185 |
else: |
|
163 | 186 |
# if we cannot determine the hostname it's better to ignore the |
164 | 187 |
# cache |
165 | 188 |
raise CacheUnusable |
... | ... | |
275 | 298 |
def json(func): |
276 | 299 |
'''Convert view to a JSON or JSON web-service supporting CORS''' |
277 | 300 |
from . import cors |
301 | ||
278 | 302 |
@wraps(func) |
279 | 303 |
def f(request, *args, **kwargs): |
280 | 304 |
jsonp = False |
src/authentic2/disco_service/disco_responder.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
""" |
2 | 18 |
Discovery Service Responder |
3 | 19 |
See Identity Provider Discovery Service Protocol and Profile |
... | ... | |
62 | 78 |
try: |
63 | 79 |
liberty_provider = LibertyProvider.objects.get(entity_id=entity_id) |
64 | 80 |
liberty_provider.service_provider |
65 |
except: |
|
66 |
logger.warn("get_disco_return_url_from_metadata: " |
|
67 |
"unknown service provider %s" \ |
|
68 |
% entity_id) |
|
81 |
except Exception: |
|
82 |
logger.warn('get_disco_return_url_from_metadata: unknown service provider %s', entity_id) |
|
69 | 83 |
return None |
70 | 84 |
dom = parseString(liberty_provider.metadata.encode('utf8')) |
71 |
endpoints = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', 'DiscoveryResponse') |
|
85 |
endpoints = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', |
|
86 |
'DiscoveryResponse') |
|
72 | 87 |
if not endpoints: |
73 |
logger.warn("get_disco_return_url_from_metadata: " |
|
74 |
"no discovery service endpoint for %s" \ |
|
75 |
% entity_id) |
|
88 |
logger.warn('get_disco_return_url_from_metadata: no discovery service endpoint for %s', entity_id) |
|
76 | 89 |
return None |
77 | 90 |
ep = None |
78 | 91 |
value = 0 |
... | ... | |
89 | 102 |
value = int(endpoint.attributes['index'].value) |
90 | 103 |
ep = endpoint |
91 | 104 |
if not ep: |
92 |
logger.warn("get_disco_return_url_from_metadata: " |
|
93 |
"no valid endpoint for %s" \ |
|
94 |
% entity_id) |
|
105 |
logger.warn("get_disco_return_url_from_metadata: no valid endpoint for %s", entity_id) |
|
95 | 106 |
return None |
96 | 107 | |
97 |
logger.debug("get_disco_return_url_from_metadata: " |
|
98 |
"found endpoint with index %s" \ |
|
99 |
% str(value)) |
|
108 |
logger.debug('get_disco_return_url_from_metadata: found endpoint with index %s', value) |
|
109 | ||
100 | 110 |
if 'Location' in ep.attributes.keys(): |
101 | 111 |
location = ep.attributes['Location'].value |
102 |
logger.debug("get_disco_return_url_from_metadata: " |
|
103 |
"location is %s" \ |
|
104 |
% location) |
|
112 |
logger.debug('get_disco_return_url_from_metadata: location is %s', location) |
|
105 | 113 |
return location |
106 | 114 | |
107 |
logger.warn("get_disco_return_url_from_metadata: " |
|
108 |
"no location found for endpoint with index %s" \ |
|
109 |
% str(value)) |
|
115 |
logger.warn('get_disco_return_url_from_metadata: no location found for endpoint with index %s', value) |
|
110 | 116 |
return None |
111 | 117 | |
112 | 118 | |
... | ... | |
145 | 151 | |
146 | 152 |
# Back from the selection interface |
147 | 153 |
if idp_selected: |
148 |
logger.info("disco: " |
|
149 |
"back from the idp selection interface with value %s" \ |
|
150 |
% idp_selected) |
|
154 |
logger.info('disco: back from the idp selection interface with value %s', idp_selected) |
|
151 | 155 | |
152 | 156 |
if not is_known_idp(idp_selected): |
153 | 157 |
message = 'The idp is unknown.' |
... | ... | |
163 | 167 |
# Discovery request parameters |
164 | 168 |
entityID = request.GET.get('entityID', '') |
165 | 169 |
_return = request.GET.get('return', '') |
166 |
policy = request.GET.get('idp_selected', |
|
167 |
'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single') |
|
170 |
policy = request.GET.get('idp_selected', 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single') |
|
168 | 171 |
returnIDParam = request.GET.get('returnIDParam', 'entityID') |
172 |
# XXX: isPassive is unused |
|
169 | 173 |
isPassive = request.GET.get('isPassive', '') |
170 | 174 |
if isPassive and isPassive == 'true': |
171 |
isPassive=True
|
|
175 |
isPassive = True
|
|
172 | 176 |
else: |
173 |
isPAssive=False
|
|
177 |
isPassive = False
|
|
174 | 178 | |
175 | 179 |
if not entityID: |
176 | 180 |
message = _('missing mandatory parameter entityID') |
177 | 181 |
return error_page(request, message, logger=logger) |
178 | 182 | |
179 |
if policy != \ |
|
180 |
'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single': |
|
183 |
if policy != 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single': |
|
181 | 184 |
message = _('policy %r not implemented') % policy |
182 | 185 |
return error_page(request, message, logger=logger) |
183 | 186 | |
... | ... | |
189 | 192 |
else: |
190 | 193 |
return_url = _return |
191 | 194 |
if not return_url: |
192 |
message = _('unable to find a valid return url for %s' \ |
|
193 |
% entityID) |
|
195 |
message = _('unable to find a valid return url for %s') % entityID |
|
194 | 196 |
return error_page(request, message, logger=logger) |
195 | 197 | |
196 | 198 |
# Check that the return_url does not already contain a param with name |
197 | 199 |
# equal to returnIDParam. Else, it is an unconformant SP. |
198 | 200 |
if is_param_id_in_return_url(return_url, returnIDParam): |
199 |
message = _('invalid return url %(return_url)s for %(entity_id)s' \
|
|
200 |
% dict(return_url=return_url, entity_id=entityID))
|
|
201 |
message = _('invalid return url %(return_url)s for %(entity_id)s') % dict(
|
|
202 |
return_url=return_url, entity_id=entityID)
|
|
201 | 203 |
return error_page(request, message, logger=logger) |
202 | 204 | |
203 | 205 |
# not back from selection interface |
... | ... | |
208 | 210 |
if not idp_selected: |
209 | 211 |
# no idp selected and we must not interect with the user |
210 | 212 |
if isPassive: |
211 |
#No IdP selected = just return to the return url |
|
213 |
# No IdP selected = just return to the return url
|
|
212 | 214 |
return HttpResponseRedirect(return_url) |
213 | 215 |
# Go to selection interface |
214 | 216 |
else: |
215 |
save_key_values(request, entityID, _return, policy, returnIDParam, |
|
216 |
isPassive) |
|
217 |
save_key_values(request, entityID, _return, policy, returnIDParam, isPassive) |
|
217 | 218 |
return HttpResponseRedirect(reverse(idp_selection)) |
218 | 219 | |
219 | 220 |
# We got it! |
220 | 221 |
set_or_refresh_prefered_idp(request, idp_selected) |
221 |
return HttpResponseRedirect(add_param_to_url(return_url, returnIDParam, |
|
222 |
idp_selected)) |
|
222 |
return HttpResponseRedirect(add_param_to_url(return_url, returnIDParam, idp_selected))
|
|
223 | ||
223 | 224 | |
224 | 225 |
def idp_selection(request): |
225 | 226 |
# XXX: Code here the IdP selection |
226 | 227 |
idp_selected = urlquote('http://www.identity-hub.com/idp/saml2/metadata') |
227 |
return HttpResponseRedirect('%s?idp_selected=%s' \ |
|
228 |
% (reverse(disco), idp_selected)) |
|
228 |
return HttpResponseRedirect('%s?idp_selected=%s' % (reverse(disco), idp_selected)) |
|
229 | 229 | |
230 | 230 |
urlpatterns = [ |
231 | 231 |
url(r'^disco$', disco), |
src/authentic2/exponential_retry_timeout.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import time |
2 | 18 |
import logging |
3 | 19 |
import hashlib |
src/authentic2/forms/__init__.py | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import math |
|
18 | ||
19 |
from django import forms |
|
20 |
from django.forms.models import modelform_factory as django_modelform_factory |
|
21 |
from django.utils.translation import ugettext_lazy as _ |
|
22 |
from django.contrib.auth import REDIRECT_FIELD_NAME, forms as auth_forms |
|
23 |
from django.utils import html |
|
24 | ||
25 |
from django.contrib.auth import authenticate |
|
26 | ||
27 |
from django_rbac.utils import get_ou_model |
|
28 | ||
29 |
from authentic2.utils import lazy_label |
|
30 |
from authentic2.compat import get_user_model |
|
31 |
from authentic2.forms.fields import PasswordField |
|
32 | ||
33 |
from .. import app_settings |
|
34 |
from ..exponential_retry_timeout import ExponentialRetryTimeout |
|
35 | ||
36 |
OU = get_ou_model() |
|
37 | ||
38 | ||
39 |
class EmailChangeFormNoPassword(forms.Form): |
|
40 |
email = forms.EmailField(label=_('New email')) |
|
41 | ||
42 |
def __init__(self, user, *args, **kwargs): |
|
43 |
self.user = user |
|
44 |
super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs) |
|
45 | ||
46 | ||
47 |
class EmailChangeForm(EmailChangeFormNoPassword): |
|
48 |
password = forms.CharField(label=_("Password"), |
|
49 |
widget=forms.PasswordInput) |
|
50 | ||
51 |
def clean_email(self): |
|
52 |
email = self.cleaned_data['email'] |
|
53 |
if email == self.user.email: |
|
54 |
raise forms.ValidationError(_('This is already your email address.')) |
|
55 |
return email |
|
56 | ||
57 |
def clean_password(self): |
|
58 |
password = self.cleaned_data["password"] |
|
59 |
if not self.user.check_password(password): |
|
60 |
raise forms.ValidationError( |
|
61 |
_('Incorrect password.'), |
|
62 |
code='password_incorrect', |
|
63 |
) |
|
64 |
return password |
|
65 | ||
66 | ||
67 |
class NextUrlFormMixin(forms.Form): |
|
68 |
next_url = forms.CharField(widget=forms.HiddenInput(), required=False) |
|
69 | ||
70 |
def __init__(self, *args, **kwargs): |
|
71 |
from authentic2.middleware import StoreRequestMiddleware |
|
72 | ||
73 |
next_url = kwargs.pop('next_url', None) |
|
74 |
request = StoreRequestMiddleware.get_request() |
|
75 |
if not next_url and request: |
|
76 |
next_url = request.GET.get(REDIRECT_FIELD_NAME) |
|
77 |
super(NextUrlFormMixin, self).__init__(*args, **kwargs) |
|
78 |
if next_url: |
|
79 |
self.fields['next_url'].initial = next_url |
|
80 | ||
81 | ||
82 |
class BaseUserForm(forms.ModelForm): |
|
83 |
error_messages = { |
|
84 |
'duplicate_username': _("A user with that username already exists."), |
|
85 |
} |
|
86 | ||
87 |
def __init__(self, *args, **kwargs): |
|
88 |
from authentic2 import models |
|
89 | ||
90 |
self.attributes = models.Attribute.objects.all() |
|
91 |
initial = kwargs.setdefault('initial', {}) |
|
92 |
if kwargs.get('instance'): |
|
93 |
instance = kwargs['instance'] |
|
94 |
for av in models.AttributeValue.objects.with_owner(instance): |
|
95 |
if av.attribute.name in self.declared_fields: |
|
96 |
if av.verified: |
|
97 |
self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly' |
|
98 |
initial[av.attribute.name] = av.to_python() |
|
99 |
super(BaseUserForm, self).__init__(*args, **kwargs) |
|
100 | ||
101 |
def clean(self): |
|
102 |
from authentic2 import models |
|
103 | ||
104 |
# make sure verified fields are not modified |
|
105 |
for av in models.AttributeValue.objects.with_owner( |
|
106 |
self.instance).filter(verified=True): |
|
107 |
self.cleaned_data[av.attribute.name] = av.to_python() |
|
108 |
super(BaseUserForm, self).clean() |
|
109 | ||
110 |
def save_attributes(self): |
|
111 |
# only save non verified attributes here |
|
112 |
verified_attributes = set( |
|
113 |
self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True) |
|
114 |
) |
|
115 |
for attribute in self.attributes: |
|
116 |
name = attribute.name |
|
117 |
if name in self.fields and name not in verified_attributes: |
|
118 |
value = self.cleaned_data[name] |
|
119 |
setattr(self.instance.attributes, name, value) |
|
120 | ||
121 |
def save(self, commit=True): |
|
122 |
result = super(BaseUserForm, self).save(commit=commit) |
|
123 |
if commit: |
|
124 |
self.save_attributes() |
|
125 |
else: |
|
126 |
old = self.save_m2m |
|
127 | ||
128 |
def save_m2m(*args, **kwargs): |
|
129 |
old(*args, **kwargs) |
|
130 |
self.save_attributes() |
|
131 |
self.save_m2m = save_m2m |
|
132 |
return result |
|
133 | ||
134 | ||
135 |
class EditProfileForm(NextUrlFormMixin, BaseUserForm): |
|
136 |
pass |
|
137 | ||
138 | ||
139 |
def modelform_factory(model, **kwargs): |
|
140 |
'''Build a modelform for the given model, |
|
141 | ||
142 |
For the user model also add attribute based fields. |
|
143 |
''' |
|
144 |
from authentic2 import models |
|
145 | ||
146 |
form = kwargs.pop('form', None) |
|
147 |
fields = kwargs.get('fields') or [] |
|
148 |
required = list(kwargs.pop('required', []) or []) |
|
149 |
d = {} |
|
150 |
# KV attributes are only supported for the user model currently |
|
151 |
modelform = None |
|
152 |
if issubclass(model, get_user_model()): |
|
153 |
if not form: |
|
154 |
form = BaseUserForm |
|
155 |
attributes = models.Attribute.objects.all() |
|
156 |
for attribute in attributes: |
|
157 |
if attribute.name not in fields: |
|
158 |
continue |
|
159 |
d[attribute.name] = attribute.get_form_field() |
|
160 |
for field in app_settings.A2_REQUIRED_FIELDS: |
|
161 |
if field not in required: |
|
162 |
required.append(field) |
|
163 |
if not form or not hasattr(form, 'Meta'): |
|
164 |
meta_d = {'model': model, 'fields': '__all__'} |
|
165 |
meta = type('Meta', (), meta_d) |
|
166 |
d['Meta'] = meta |
|
167 |
if not form: # fallback |
|
168 |
form = forms.ModelForm |
|
169 |
modelform = None |
|
170 |
if required: |
|
171 |
def __init__(self, *args, **kwargs): |
|
172 |
super(modelform, self).__init__(*args, **kwargs) |
|
173 |
for field in required: |
|
174 |
if field in self.fields: |
|
175 |
self.fields[field].required = True |
|
176 |
d['__init__'] = __init__ |
|
177 |
modelform = type(model.__name__ + 'ModelForm', (form,), d) |
|
178 |
kwargs['form'] = modelform |
|
179 |
modelform.required_css_class = 'form-field-required' |
|
180 |
return django_modelform_factory(model, **kwargs) |
|
181 | ||
182 | ||
183 |
class AuthenticationForm(auth_forms.AuthenticationForm): |
|
184 |
password = PasswordField(label=_('Password')) |
|
185 |
remember_me = forms.BooleanField( |
|
186 |
initial=False, |
|
187 |
required=False, |
|
188 |
label=_('Remember me'), |
|
189 |
help_text=_('Do not ask for authentication next time')) |
|
190 |
ou = forms.ModelChoiceField( |
|
191 |
label=lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL), |
|
192 |
required=True, |
|
193 |
queryset=OU.objects.all()) |
|
194 | ||
195 |
def __init__(self, *args, **kwargs): |
|
196 |
super(AuthenticationForm, self).__init__(*args, **kwargs) |
|
197 |
self.exponential_backoff = ExponentialRetryTimeout( |
|
198 |
key_prefix='login-exp-backoff-', |
|
199 |
duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION, |
|
200 |
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR) |
|
201 | ||
202 |
if not app_settings.A2_USER_REMEMBER_ME: |
|
203 |
del self.fields['remember_me'] |
|
204 | ||
205 |
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR: |
|
206 |
del self.fields['ou'] |
|
207 | ||
208 |
if self.request: |
|
209 |
self.remote_addr = self.request.META['REMOTE_ADDR'] |
|
210 |
else: |
|
211 |
self.remote_addr = '0.0.0.0' |
|
212 | ||
213 |
def exp_backoff_keys(self): |
|
214 |
return self.cleaned_data['username'], self.remote_addr |
|
215 | ||
216 |
def clean(self): |
|
217 |
username = self.cleaned_data.get('username') |
|
218 |
password = self.cleaned_data.get('password') |
|
219 | ||
220 |
keys = None |
|
221 |
if username and password: |
|
222 |
keys = self.exp_backoff_keys() |
|
223 |
seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys) |
|
224 |
if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION: |
|
225 |
seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION |
|
226 |
msg = _('You made too many login errors recently, you must ' |
|
227 |
'wait <span class="js-seconds-until">%s</span> seconds ' |
|
228 |
'to try again.') |
|
229 |
msg = msg % int(math.ceil(seconds_to_wait)) |
|
230 |
msg = html.mark_safe(msg) |
|
231 |
raise forms.ValidationError(msg) |
|
232 | ||
233 |
try: |
|
234 |
self.clean_authenticate() |
|
235 |
except Exception: |
|
236 |
if keys: |
|
237 |
self.exponential_backoff.failure(*keys) |
|
238 |
raise |
|
239 |
else: |
|
240 |
if keys: |
|
241 |
self.exponential_backoff.success(*keys) |
|
242 |
return self.cleaned_data |
|
243 | ||
244 |
def clean_authenticate(self): |
|
245 |
# copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector |
|
246 |
username = self.cleaned_data.get('username') |
|
247 |
password = self.cleaned_data.get('password') |
|
248 |
ou = self.cleaned_data.get('ou') |
|
249 | ||
250 |
if username is not None and password: |
|
251 |
self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request) |
|
252 |
if self.user_cache is None: |
|
253 |
raise forms.ValidationError( |
|
254 |
self.error_messages['invalid_login'], |
|
255 |
code='invalid_login', |
|
256 |
params={'username': self.username_field.verbose_name}, |
|
257 |
) |
|
258 |
else: |
|
259 |
self.confirm_login_allowed(self.user_cache) |
|
260 | ||
261 |
return self.cleaned_data |
|
262 | ||
263 |
@property |
|
264 |
def media(self): |
|
265 |
media = super(AuthenticationForm, self).media |
|
266 |
media.add_js(['authentic2/js/js_seconds_until.js']) |
|
267 |
if app_settings.A2_LOGIN_FORM_OU_SELECTOR: |
|
268 |
media.add_js(['authentic2/js/ou_selector.js']) |
|
269 |
return media |
|
270 | ||
271 | ||
272 |
class SiteImportForm(forms.Form): |
|
273 |
site_json = forms.FileField(label=_('Site Export File')) |
src/authentic2/forms/authentication.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import math |
|
18 | ||
19 |
from django import forms |
|
20 |
from django.utils.translation import ugettext_lazy as _ |
|
21 |
from django.contrib.auth import forms as auth_forms |
|
22 |
from django.utils import html |
|
23 | ||
24 |
from django.contrib.auth import authenticate |
|
25 | ||
26 |
from authentic2.forms.fields import PasswordField |
|
27 | ||
28 |
from ..a2_rbac.models import OrganizationalUnit as OU |
|
29 |
from .. import app_settings, utils |
|
30 |
from ..exponential_retry_timeout import ExponentialRetryTimeout |
|
31 | ||
32 | ||
33 |
class AuthenticationForm(auth_forms.AuthenticationForm): |
|
34 |
password = PasswordField(label=_('Password')) |
|
35 |
remember_me = forms.BooleanField( |
|
36 |
initial=False, |
|
37 |
required=False, |
|
38 |
label=_('Remember me'), |
|
39 |
help_text=_('Do not ask for authentication next time')) |
|
40 |
ou = forms.ModelChoiceField( |
|
41 |
label=utils.lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL), |
|
42 |
required=True, |
|
43 |
queryset=OU.objects.all()) |
|
44 | ||
45 |
def __init__(self, *args, **kwargs): |
|
46 |
super(AuthenticationForm, self).__init__(*args, **kwargs) |
|
47 |
self.exponential_backoff = ExponentialRetryTimeout( |
|
48 |
key_prefix='login-exp-backoff-', |
|
49 |
duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION, |
|
50 |
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR) |
|
51 | ||
52 |
if not app_settings.A2_USER_REMEMBER_ME: |
|
53 |
del self.fields['remember_me'] |
|
54 | ||
55 |
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR: |
|
56 |
del self.fields['ou'] |
|
57 | ||
58 |
if self.request: |
|
59 |
self.remote_addr = self.request.META['REMOTE_ADDR'] |
|
60 |
else: |
|
61 |
self.remote_addr = '0.0.0.0' |
|
62 | ||
63 |
def exp_backoff_keys(self): |
|
64 |
return self.cleaned_data['username'], self.remote_addr |
|
65 | ||
66 |
def clean(self): |
|
67 |
username = self.cleaned_data.get('username') |
|
68 |
password = self.cleaned_data.get('password') |
|
69 | ||
70 |
keys = None |
|
71 |
if username and password: |
|
72 |
keys = self.exp_backoff_keys() |
|
73 |
seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys) |
|
74 |
if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION: |
|
75 |
seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION |
|
76 |
msg = _('You made too many login errors recently, you must ' |
|
77 |
'wait <span class="js-seconds-until">%s</span> seconds ' |
|
78 |
'to try again.') |
|
79 |
msg = msg % int(math.ceil(seconds_to_wait)) |
|
80 |
msg = html.mark_safe(msg) |
|
81 |
raise forms.ValidationError(msg) |
|
82 | ||
83 |
try: |
|
84 |
self.clean_authenticate() |
|
85 |
except Exception: |
|
86 |
if keys: |
|
87 |
self.exponential_backoff.failure(*keys) |
|
88 |
raise |
|
89 |
else: |
|
90 |
if keys: |
|
91 |
self.exponential_backoff.success(*keys) |
|
92 |
return self.cleaned_data |
|
93 | ||
94 |
def clean_authenticate(self): |
|
95 |
# copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector |
|
96 |
username = self.cleaned_data.get('username') |
|
97 |
password = self.cleaned_data.get('password') |
|
98 |
ou = self.cleaned_data.get('ou') |
|
99 | ||
100 |
if username is not None and password: |
|
101 |
self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request) |
|
102 |
if self.user_cache is None: |
|
103 |
raise forms.ValidationError( |
|
104 |
self.error_messages['invalid_login'], |
|
105 |
code='invalid_login', |
|
106 |
params={'username': self.username_field.verbose_name}, |
|
107 |
) |
|
108 |
else: |
|
109 |
self.confirm_login_allowed(self.user_cache) |
|
110 | ||
111 |
return self.cleaned_data |
|
112 | ||
113 |
@property |
|
114 |
def media(self): |
|
115 |
media = super(AuthenticationForm, self).media |
|
116 |
media.add_js(['authentic2/js/js_seconds_until.js']) |
|
117 |
if app_settings.A2_LOGIN_FORM_OU_SELECTOR: |
|
118 |
media.add_js(['authentic2/js/ou_selector.js']) |
|
119 |
return media |
src/authentic2/forms/fields.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import warnings |
2 | 18 |
import io |
3 | 19 |
src/authentic2/forms/passwords.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import logging |
|
18 |
from collections import OrderedDict |
|
19 | ||
20 |
from django.contrib.auth import forms as auth_forms |
|
21 |
from django.core.exceptions import ValidationError |
|
22 |
from django.forms import Form |
|
23 |
from django import forms |
|
24 |
from django.utils.translation import ugettext_lazy as _ |
|
25 | ||
26 |
from .. import models, hooks, app_settings, utils |
|
27 |
from ..backends import get_user_queryset |
|
28 |
from .fields import PasswordField, NewPasswordField, CheckPasswordField |
|
29 |
from .utils import NextUrlFormMixin |
|
30 | ||
31 | ||
32 |
logger = logging.getLogger(__name__) |
|
33 | ||
34 | ||
35 |
class PasswordResetForm(forms.Form): |
|
36 |
next_url = forms.CharField(widget=forms.HiddenInput, required=False) |
|
37 | ||
38 |
email = forms.EmailField( |
|
39 |
label=_("Email"), max_length=254) |
|
40 | ||
41 |
def save(self): |
|
42 |
""" |
|
43 |
Generates a one-use only link for resetting password and sends to the |
|
44 |
user. |
|
45 |
""" |
|
46 |
email = self.cleaned_data["email"].strip() |
|
47 |
users = get_user_queryset() |
|
48 |
active_users = users.filter(email__iexact=email, is_active=True) |
|
49 |
for user in active_users: |
|
50 |
# we don't set the password to a random string, as some users should not have |
|
51 |
# a password |
|
52 |
set_random_password = (user.has_usable_password() |
|
53 |
and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET) |
|
54 |
utils.send_password_reset_mail( |
|
55 |
user, |
|
56 |
set_random_password=set_random_password, |
|
57 |
next_url=self.cleaned_data.get('next_url')) |
|
58 |
if not active_users: |
|
59 |
logger.info(u'password reset requests for "%s", no user found') |
|
60 |
hooks.call_hooks('event', name='password-reset', email=email, users=active_users) |
|
61 | ||
62 | ||
63 |
class PasswordResetMixin(Form): |
|
64 |
'''Remove all password reset object for the current user when password is |
|
65 |
successfully changed.''' |
|
66 | ||
67 |
def save(self, commit=True): |
|
68 |
ret = super(PasswordResetMixin, self).save(commit=commit) |
|
69 |
if commit: |
|
70 |
models.PasswordReset.objects.filter(user=self.user).delete() |
|
71 |
else: |
|
72 |
old_save = self.user.save |
|
73 | ||
74 |
def save(*args, **kwargs): |
|
75 |
ret = old_save(*args, **kwargs) |
|
76 |
models.PasswordReset.objects.filter(user=self.user).delete() |
|
77 |
return ret |
|
78 |
self.user.save = save |
|
79 |
return ret |
|
80 | ||
81 | ||
82 |
class NotifyOfPasswordChange(object): |
|
83 |
def save(self, commit=True): |
|
84 |
user = super(NotifyOfPasswordChange, self).save(commit=commit) |
|
85 |
if user.email: |
|
86 |
ctx = { |
|
87 |
'user': user, |
|
88 |
'password': self.cleaned_data['new_password1'], |
|
89 |
} |
|
90 |
utils.send_templated_mail(user, "authentic2/password_change", ctx) |
|
91 |
return user |
|
92 | ||
93 | ||
94 |
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm): |
|
95 |
new_password1 = NewPasswordField(label=_("New password")) |
|
96 |
new_password2 = CheckPasswordField(label=_("New password confirmation")) |
|
97 | ||
98 |
def clean_new_password1(self): |
|
99 |
new_password1 = self.cleaned_data.get('new_password1') |
|
100 |
if new_password1 and self.user.check_password(new_password1): |
|
101 |
raise ValidationError(_('New password must differ from old password')) |
|
102 |
return new_password1 |
|
103 | ||
104 | ||
105 |
class PasswordChangeForm(NotifyOfPasswordChange, NextUrlFormMixin, PasswordResetMixin, |
|
106 |
auth_forms.PasswordChangeForm): |
|
107 |
old_password = PasswordField(label=_('Old password')) |
|
108 |
new_password1 = NewPasswordField(label=_('New password')) |
|
109 |
new_password2 = CheckPasswordField(label=_("New password confirmation")) |
|
110 | ||
111 |
def clean_new_password1(self): |
|
112 |
new_password1 = self.cleaned_data.get('new_password1') |
|
113 |
old_password = self.cleaned_data.get('old_password') |
|
114 |
if new_password1 and new_password1 == old_password: |
|
115 |
raise ValidationError(_('New password must differ from old password')) |
|
116 |
return new_password1 |
|
117 | ||
118 |
# make old_password the first field |
|
119 |
new_base_fields = OrderedDict() |
|
120 | ||
121 |
for k in ['old_password', 'new_password1', 'new_password2']: |
|
122 |
new_base_fields[k] = PasswordChangeForm.base_fields[k] |
|
123 | ||
124 |
for k in PasswordChangeForm.base_fields: |
|
125 |
if k not in ['old_password', 'new_password1', 'new_password2']: |
|
126 |
new_base_fields[k] = PasswordChangeForm.base_fields[k] |
|
127 | ||
128 |
PasswordChangeForm.base_fields = new_base_fields |
src/authentic2/forms/profile.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 | ||
18 |
from django.forms.models import modelform_factory as dj_modelform_factory |
|
19 |
from django import forms |
|
20 |
from django.utils.translation import ugettext_lazy as _, ugettext |
|
21 | ||
22 |
from ..custom_user.models import User |
|
23 |
from .. import app_settings, models |
|
24 |
from .utils import NextUrlFormMixin |
|
25 | ||
26 | ||
27 |
class DeleteAccountForm(forms.Form): |
|
28 |
password = forms.CharField(widget=forms.PasswordInput, label=_("Password")) |
|
29 | ||
30 |
def __init__(self, *args, **kwargs): |
|
31 |
self.user = kwargs.pop('user') |
|
32 |
super(DeleteAccountForm, self).__init__(*args, **kwargs) |
|
33 | ||
34 |
def clean_password(self): |
|
35 |
password = self.cleaned_data.get('password') |
|
36 |
if password and not self.user.check_password(password): |
|
37 |
raise forms.ValidationError(ugettext('Password is invalid')) |
|
38 |
return password |
|
39 | ||
40 | ||
41 |
class EmailChangeFormNoPassword(forms.Form): |
|
42 |
email = forms.EmailField(label=_('New email')) |
|
43 | ||
44 |
def __init__(self, user, *args, **kwargs): |
|
45 |
self.user = user |
|
46 |
super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs) |
|
47 | ||
48 | ||
49 |
class EmailChangeForm(EmailChangeFormNoPassword): |
|
50 |
password = forms.CharField(label=_("Password"), |
|
51 |
widget=forms.PasswordInput) |
|
52 | ||
53 |
def clean_email(self): |
|
54 |
email = self.cleaned_data['email'] |
|
55 |
if email == self.user.email: |
|
56 |
raise forms.ValidationError(_('This is already your email address.')) |
|
57 |
return email |
|
58 | ||
59 |
def clean_password(self): |
|
60 |
password = self.cleaned_data["password"] |
|
61 |
if not self.user.check_password(password): |
|
62 |
raise forms.ValidationError( |
|
63 |
_('Incorrect password.'), |
|
64 |
code='password_incorrect', |
|
65 |
) |
|
66 |
return password |
|
67 | ||
68 | ||
69 |
class BaseUserForm(forms.ModelForm): |
|
70 |
error_messages = { |
|
71 |
'duplicate_username': _("A user with that username already exists."), |
|
72 |
} |
|
73 | ||
74 |
def __init__(self, *args, **kwargs): |
|
75 |
from authentic2 import models |
|
76 | ||
77 |
self.attributes = models.Attribute.objects.all() |
|
78 |
initial = kwargs.setdefault('initial', {}) |
|
79 |
if kwargs.get('instance'): |
|
80 |
instance = kwargs['instance'] |
|
81 |
for av in models.AttributeValue.objects.with_owner(instance): |
|
82 |
if av.attribute.name in self.declared_fields: |
|
83 |
if av.verified: |
|
84 |
self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly' |
|
85 |
initial[av.attribute.name] = av.to_python() |
|
86 |
super(BaseUserForm, self).__init__(*args, **kwargs) |
|
87 | ||
88 |
def clean(self): |
|
89 |
from authentic2 import models |
|
90 | ||
91 |
# make sure verified fields are not modified |
|
92 |
for av in models.AttributeValue.objects.with_owner( |
|
93 |
self.instance).filter(verified=True): |
|
94 |
self.cleaned_data[av.attribute.name] = av.to_python() |
|
95 |
super(BaseUserForm, self).clean() |
|
96 | ||
97 |
def save_attributes(self): |
|
98 |
# only save non verified attributes here |
|
99 |
verified_attributes = set( |
|
100 |
self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True) |
|
101 |
) |
|
102 |
for attribute in self.attributes: |
|
103 |
name = attribute.name |
|
104 |
if name in self.fields and name not in verified_attributes: |
|
105 |
value = self.cleaned_data[name] |
|
106 |
setattr(self.instance.attributes, name, value) |
|
107 | ||
108 |
def save(self, commit=True): |
|
109 |
result = super(BaseUserForm, self).save(commit=commit) |
|
110 |
if commit: |
|
111 |
self.save_attributes() |
|
112 |
else: |
|
113 |
old = self.save_m2m |
|
114 | ||
115 |
def save_m2m(*args, **kwargs): |
|
116 |
old(*args, **kwargs) |
|
117 |
self.save_attributes() |
|
118 |
self.save_m2m = save_m2m |
|
119 |
return result |
|
120 | ||
121 | ||
122 |
class EditProfileForm(NextUrlFormMixin, BaseUserForm): |
|
123 |
pass |
|
124 | ||
125 | ||
126 |
def modelform_factory(model, **kwargs): |
|
127 |
'''Build a modelform for the given model, |
|
128 | ||
129 |
For the user model also add attribute based fields. |
|
130 |
''' |
|
131 | ||
132 |
form = kwargs.pop('form', None) |
|
133 |
fields = kwargs.get('fields') or [] |
|
134 |
required = list(kwargs.pop('required', []) or []) |
|
135 |
d = {} |
|
136 |
# KV attributes are only supported for the user model currently |
|
137 |
modelform = None |
|
138 |
if issubclass(model, User): |
|
139 |
if not form: |
|
140 |
form = BaseUserForm |
|
141 |
attributes = models.Attribute.objects.all() |
|
142 |
for attribute in attributes: |
|
143 |
if attribute.name not in fields: |
|
144 |
continue |
|
145 |
d[attribute.name] = attribute.get_form_field() |
|
146 |
for field in app_settings.A2_REQUIRED_FIELDS: |
|
147 |
if field not in required: |
|
148 |
required.append(field) |
|
149 |
if not form or not hasattr(form, 'Meta'): |
|
150 |
meta_d = {'model': model, 'fields': '__all__'} |
|
151 |
meta = type('Meta', (), meta_d) |
|
152 |
d['Meta'] = meta |
|
153 |
if not form: # fallback |
|
154 |
form = forms.ModelForm |
|
155 |
modelform = None |
|
156 |
if required: |
|
157 |
def __init__(self, *args, **kwargs): |
|
158 |
super(modelform, self).__init__(*args, **kwargs) |
|
159 |
for field in required: |
|
160 |
if field in self.fields: |
|
161 |
self.fields[field].required = True |
|
162 |
d['__init__'] = __init__ |
|
163 |
modelform = type(model.__name__ + 'ModelForm', (form,), d) |
|
164 |
kwargs['form'] = modelform |
|
165 |
modelform.required_css_class = 'form-field-required' |
|
166 |
return dj_modelform_factory(model, **kwargs) |
|
167 | ||
168 |
src/authentic2/registration_backend/forms.py → src/authentic2/forms/registration.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import re |
2 |
import copy |
|
3 |
from collections import OrderedDict |
|
4 | 18 | |
5 |
from django.conf import settings
|
|
19 |
from django.contrib.auth import get_user_model
|
|
6 | 20 |
from django.core.exceptions import ValidationError |
7 | 21 |
from django.utils.translation import ugettext_lazy as _, ugettext |
8 |
from django.forms import ModelForm, Form, CharField, PasswordInput, EmailField |
|
9 |
from django.db.models.fields import FieldDoesNotExist |
|
10 |
from django.forms.utils import ErrorList |
|
22 |
from django.forms import Form, EmailField |
|
11 | 23 | |
12 | 24 |
from django.contrib.auth.models import BaseUserManager, Group |
13 |
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME |
|
14 |
from django.core.mail import send_mail |
|
15 |
from django.core import signing |
|
16 |
from django.template import RequestContext |
|
17 |
from django.template.loader import render_to_string |
|
18 |
from django.core.urlresolvers import reverse |
|
19 |
from django.core.validators import RegexValidator |
|
20 | ||
21 |
from authentic2.forms.fields import PasswordField, NewPasswordField, CheckPasswordField |
|
22 |
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks |
|
25 | ||
26 |
from authentic2.forms.fields import NewPasswordField, CheckPasswordField |
|
23 | 27 |
from authentic2.a2_rbac.models import OrganizationalUnit |
24 | 28 | |
25 |
User = compat.get_user_model() |
|
29 |
from .. import app_settings, models |
|
30 |
from . import profile as profile_forms |
|
31 | ||
32 |
User = get_user_model() |
|
26 | 33 | |
27 | 34 | |
28 | 35 |
class RegistrationForm(Form): |
... | ... | |
53 | 60 |
return email |
54 | 61 | |
55 | 62 | |
56 |
class RegistrationCompletionFormNoPassword(forms.BaseUserForm): |
|
63 |
class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm):
|
|
57 | 64 |
error_css_class = 'form-field-error' |
58 | 65 |
required_css_class = 'form-field-required' |
59 | 66 | |
... | ... | |
67 | 74 |
ou = OrganizationalUnit.objects.get(pk=self.data['ou']) |
68 | 75 |
username_is_unique |= ou.username_is_unique |
69 | 76 |
if username_is_unique: |
70 |
User = get_user_model() |
|
71 | 77 |
exist = False |
72 | 78 |
try: |
73 | 79 |
User.objects.get(username=username) |
... | ... | |
86 | 92 |
if self.cleaned_data.get('email'): |
87 | 93 |
email = self.cleaned_data['email'] |
88 | 94 |
if app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE: |
89 |
User = get_user_model() |
|
90 | 95 |
exist = False |
91 | 96 |
try: |
92 | 97 |
User.objects.get(email__iexact=email) |
... | ... | |
130 | 135 |
raise ValidationError(_("The two password fields didn't match.")) |
131 | 136 |
self.instance.set_password(self.cleaned_data['password1']) |
132 | 137 |
return self.cleaned_data |
133 | ||
134 | ||
135 |
class PasswordResetMixin(Form): |
|
136 |
'''Remove all password reset object for the current user when password is |
|
137 |
successfully changed.''' |
|
138 | ||
139 |
def save(self, commit=True): |
|
140 |
ret = super(PasswordResetMixin, self).save(commit=commit) |
|
141 |
if commit: |
|
142 |
models.PasswordReset.objects.filter(user=self.user).delete() |
|
143 |
else: |
|
144 |
old_save = self.user.save |
|
145 |
def save(*args, **kwargs): |
|
146 |
ret = old_save(*args, **kwargs) |
|
147 |
models.PasswordReset.objects.filter(user=self.user).delete() |
|
148 |
return ret |
|
149 |
self.user.save = save |
|
150 |
return ret |
|
151 | ||
152 | ||
153 |
class NotifyOfPasswordChange(object): |
|
154 |
def save(self, commit=True): |
|
155 |
user = super(NotifyOfPasswordChange, self).save(commit=commit) |
|
156 |
if user.email: |
|
157 |
ctx = { |
|
158 |
'user': user, |
|
159 |
'password': self.cleaned_data['new_password1'], |
|
160 |
} |
|
161 |
utils.send_templated_mail(user, "authentic2/password_change", ctx) |
|
162 |
return user |
|
163 | ||
164 | ||
165 |
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm): |
|
166 |
new_password1 = NewPasswordField(label=_("New password")) |
|
167 |
new_password2 = CheckPasswordField(label=_("New password confirmation")) |
|
168 | ||
169 |
def clean_new_password1(self): |
|
170 |
new_password1 = self.cleaned_data.get('new_password1') |
|
171 |
if new_password1 and self.user.check_password(new_password1): |
|
172 |
raise ValidationError(_('New password must differ from old password')) |
|
173 |
return new_password1 |
|
174 | ||
175 | ||
176 |
class PasswordChangeForm(NotifyOfPasswordChange, forms.NextUrlFormMixin, PasswordResetMixin, |
|
177 |
auth_forms.PasswordChangeForm): |
|
178 |
old_password = PasswordField(label=_('Old password')) |
|
179 |
new_password1 = NewPasswordField(label=_('New password')) |
|
180 |
new_password2 = CheckPasswordField(label=_("New password confirmation")) |
|
181 | ||
182 |
def clean_new_password1(self): |
|
183 |
new_password1 = self.cleaned_data.get('new_password1') |
|
184 |
old_password = self.cleaned_data.get('old_password') |
|
185 |
if new_password1 and new_password1 == old_password: |
|
186 |
raise ValidationError(_('New password must differ from old password')) |
|
187 |
return new_password1 |
|
188 | ||
189 |
# make old_password the first field |
|
190 |
PasswordChangeForm.base_fields = OrderedDict( |
|
191 |
[(k, PasswordChangeForm.base_fields[k]) |
|
192 |
for k in ['old_password', 'new_password1', 'new_password2']] + |
|
193 |
[(k, PasswordChangeForm.base_fields[k]) |
|
194 |
for k in PasswordChangeForm.base_fields if k not in ['old_password', 'new_password1', |
|
195 |
'new_password2']] |
|
196 |
) |
|
197 | ||
198 |
class DeleteAccountForm(Form): |
|
199 |
password = CharField(widget=PasswordInput, label=_("Password")) |
|
200 | ||
201 |
def __init__(self, *args, **kwargs): |
|
202 |
self.user = kwargs.pop('user') |
|
203 |
super(DeleteAccountForm, self).__init__(*args, **kwargs) |
|
204 | ||
205 |
def clean_password(self): |
|
206 |
password = self.cleaned_data.get('password') |
|
207 |
if password and not self.user.check_password(password): |
|
208 |
raise ValidationError(ugettext('Password is invalid')) |
|
209 |
return password |
src/authentic2/forms/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django import forms |
|
18 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
|
19 | ||
20 |
from ..middleware import StoreRequestMiddleware |
|
21 | ||
22 | ||
23 |
class NextUrlFormMixin(forms.Form): |
|
24 |
next_url = forms.CharField(widget=forms.HiddenInput(), required=False) |
|
25 | ||
26 |
def __init__(self, *args, **kwargs): |
|
27 |
next_url = kwargs.pop('next_url', None) |
|
28 |
request = StoreRequestMiddleware.get_request() |
|
29 |
if not next_url and request: |
|
30 |
next_url = request.GET.get(REDIRECT_FIELD_NAME) |
|
31 |
super(NextUrlFormMixin, self).__init__(*args, **kwargs) |
|
32 |
if next_url: |
|
33 |
self.fields['next_url'].initial = next_url |
src/authentic2/forms/widgets.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
# Bootstrap django-datetime-widget is a simple and clean widget for DateField, |
2 | 18 |
# Timefiled and DateTimeField in Django framework. It is based on Bootstrap |
3 | 19 |
# datetime picker, supports Bootstrap 2 |
... | ... | |
12 | 28 |
import uuid |
13 | 29 | |
14 | 30 |
import django |
15 |
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \ |
|
16 |
ClearableFileInput |
|
31 |
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, ClearableFileInput |
|
17 | 32 |
from django.forms.widgets import PasswordInput as BasePasswordInput |
18 | 33 |
from django.utils.formats import get_language, get_format |
19 | 34 |
from django.utils.safestring import mark_safe |
... | ... | |
95 | 110 |
date_format = self.options['format'] |
96 | 111 |
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub( |
97 | 112 |
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], |
98 |
date_format |
|
99 |
) |
|
113 |
date_format) |
|
100 | 114 | |
101 | 115 |
super(PickerWidgetMixin, self).__init__(attrs, format=self.format) |
102 | 116 | |
... | ... | |
112 | 126 |
final_attrs['class'] = "controls input-append date" |
113 | 127 |
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs) |
114 | 128 | |
115 |
#if not set, autoclose have to be true. |
|
129 |
# if not set, autoclose have to be true.
|
|
116 | 130 |
self.options.setdefault('autoclose', True) |
117 | 131 | |
118 | 132 |
# Build javascript options out of python dictionary |
... | ... | |
130 | 144 |
help_text = u'%s %s' % (_('Format:'), self.options['format']) |
131 | 145 | |
132 | 146 |
return mark_safe(BOOTSTRAP_INPUT_TEMPLATE % dict( |
133 |
id=id, |
|
134 |
rendered_widget=rendered_widget, |
|
135 |
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '', |
|
136 |
glyphicon=self.glyphicon, |
|
137 |
options=js_options, |
|
138 |
help_text=help_text, |
|
139 |
) |
|
140 |
) |
|
147 |
id=id, |
|
148 |
rendered_widget=rendered_widget, |
|
149 |
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '', |
|
150 |
glyphicon=self.glyphicon, |
|
151 |
options=js_options, |
|
152 |
help_text=help_text)) |
|
141 | 153 | |
142 | 154 | |
143 | 155 |
class DateTimeWidget(PickerWidgetMixin, DateTimeInput): |
... | ... | |
253 | 265 |
class ProfileImageInput(ClearableFileInput): |
254 | 266 |
if django.VERSION < (1, 9): |
255 | 267 |
template_with_initial = ( |
256 |
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> ' |
|
257 |
'%(clear_template)s<br />%(input_text)s: %(input)s' |
|
268 |
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> '
|
|
269 |
'%(clear_template)s<br />%(input_text)s: %(input)s'
|
|
258 | 270 |
) |
259 | 271 |
else: |
260 | 272 |
template_name = "authentic2/profile_image_input.html" |
src/authentic2/hashers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import hashlib |
2 | 18 |
import math |
3 | 19 |
import base64 |
... | ... | |
66 | 82 |
assert salt and '$' not in salt |
67 | 83 |
h = salt |
68 | 84 |
password = force_bytes(password) |
69 |
for i in xrange(iterations+1):
|
|
85 |
for i in range(iterations + 1):
|
|
70 | 86 |
h = self.digest(h + password).digest() |
71 | 87 |
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, self.b64encode(h)[:43]) |
72 | 88 | |
... | ... | |
117 | 133 | |
118 | 134 | |
119 | 135 |
OPENLDAP_ALGO_MAPPING = { |
120 |
# hasher? salt offset? hex encode? |
|
121 |
'SHA': ( 'sha-oldap', 0, True), |
|
122 |
'SSHA': ('ssha-oldap', 20, True), |
|
123 |
'MD5': ( 'md5-oldap', 0, True), |
|
124 |
'SMD5': ( 'md5-oldap', 16, True), |
|
136 |
'SHA': ( |
|
137 |
'sha-oldap', |
|
138 |
0, |
|
139 |
True |
|
140 |
), |
|
141 |
'SSHA': ( |
|
142 |
'ssha-oldap', |
|
143 |
20, |
|
144 |
True |
|
145 |
), |
|
146 |
'MD5': ( |
|
147 |
'md5-oldap', |
|
148 |
0, |
|
149 |
True |
|
150 |
), |
|
151 |
'SMD5': ( |
|
152 |
'md5-oldap', |
|
153 |
16, |
|
154 |
True |
|
155 |
), |
|
125 | 156 |
} |
126 | 157 | |
127 | 158 |
src/authentic2/hooks.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 | 19 |
from django.apps import apps |
... | ... | |
37 | 53 |
for hook in hooks: |
38 | 54 |
try: |
39 | 55 |
yield hook(*args, **kwargs) |
40 |
except: |
|
56 |
except Exception:
|
|
41 | 57 |
logger.exception(u'exception while calling hook %s', hook) |
42 | 58 | |
43 | 59 | |
... | ... | |
50 | 66 |
result = hook(*args, **kwargs) |
51 | 67 |
if result is not None: |
52 | 68 |
return result |
53 |
except: |
|
69 |
except Exception:
|
|
54 | 70 |
logger.exception(u'exception while calling hook %s', hook) |
src/authentic2/http_utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 | |
2 | 18 |
import requests |
3 | 19 | |
4 | 20 |
from authentic2 import app_settings |
5 | 21 | |
22 | ||
6 | 23 |
def get_url(url): |
7 | 24 |
'''Does a simple GET on an URL, check the certificate''' |
8 | 25 |
verify = app_settings.A2_VERIFY_SSL |
src/authentic2/idp/interactions.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib.auth.decorators import login_required |
2 | 18 |
from django.http import HttpResponseRedirect |
3 | 19 |
from django.shortcuts import render |
4 | 20 | |
5 |
from authentic2.saml.models import LibertyProvider |
|
6 | ||
7 | 21 | |
8 | 22 |
@login_required |
9 |
def consent_federation(request, nonce = '', next = None, provider_id = None):
|
|
23 |
def consent_federation(request, nonce='', provider_id=None):
|
|
10 | 24 |
'''On a GET produce a form asking for consentment, |
11 | 25 |
On a POST handle the form and redirect to next''' |
12 | 26 |
if request.method == "GET": |
13 |
return render(request, 'interaction/consent_federation.html', |
|
14 |
{'provider_id': request.GET.get('provider_id', ''), |
|
15 |
'nonce': request.GET.get('nonce', ''), |
|
16 |
'next': request.GET.get('next', '')}) |
|
27 |
return render( |
|
28 |
request, 'interaction/consent_federation.html', |
|
29 |
{ |
|
30 |
'provider_id': request.GET.get('provider_id', ''), |
|
31 |
'nonce': request.GET.get('nonce', ''), |
|
32 |
'next': request.GET.get('next', '') |
|
33 |
}) |
|
17 | 34 |
else: |
18 |
next = '/' |
|
35 |
next_url = '/'
|
|
19 | 36 |
if 'next' in request.POST: |
20 |
next = request.POST['next'] |
|
37 |
next_url = request.POST['next']
|
|
21 | 38 |
if 'accept' in request.POST: |
22 |
next = next + '&consent_answer=accepted'
|
|
23 |
return HttpResponseRedirect(next) |
|
39 |
next_url = next_url + '&consent_answer=accepted'
|
|
40 |
return HttpResponseRedirect(next_url)
|
|
24 | 41 |
else: |
25 |
next = next + '&consent_answer=refused'
|
|
42 |
next_url = next_url + '&consent_answer=refused'
|
|
26 | 43 |
return HttpResponseRedirect(next) |
27 | ||
28 |
@login_required |
|
29 |
def consent_attributes(request, nonce = '', next = None, provider_id = None): |
|
30 |
'''On a GET produce a form asking for consentment, |
|
31 |
On a POST handle the form and redirect to next''' |
|
32 |
provider = None |
|
33 |
try: |
|
34 |
provider = LibertyProvider.objects.get(entity_id=request.GET.get('provider_id', '')) |
|
35 |
except: |
|
36 |
pass |
|
37 |
next = '/' |
|
38 | ||
39 |
if request.method == "GET": |
|
40 |
attributes = [] |
|
41 |
next = request.GET.get('next', '') |
|
42 |
if 'attributes_to_send' in request.session: |
|
43 |
i = 0 |
|
44 |
for key, values in request.session['attributes_to_send'].items(): |
|
45 |
name = None |
|
46 |
if type(key) is tuple and len(key) == 3: |
|
47 |
_, _, name = key |
|
48 |
elif type(key) is tuple and len(key) == 2: |
|
49 |
name, _, = key |
|
50 |
else: |
|
51 |
name = key |
|
52 |
if name and values: |
|
53 |
attributes.append((i, name, values)) |
|
54 |
i = i + 1 |
|
55 |
name = request.GET.get('provider_id', '') |
|
56 |
if provider: |
|
57 |
name = provider.name or name |
|
58 |
return render(request, 'interaction/consent_attributes.html', |
|
59 |
{'provider_id': name, |
|
60 |
'attributes': attributes, |
|
61 |
'allow_selection': request.session['allow_attributes_selection'], |
|
62 |
'nonce': request.GET.get('nonce', ''), |
|
63 |
'next': next}) |
|
64 | ||
65 |
elif request.method == "POST": |
|
66 |
if request.session['allow_attributes_selection']: |
|
67 |
vals = \ |
|
68 |
[int(value) for key, value in request.POST.items() \ |
|
69 |
if 'attribute_nb' in key] |
|
70 |
attributes_to_send = dict() |
|
71 |
i = 0 |
|
72 |
for k, v in request.session['attributes_to_send'].items(): |
|
73 |
if i in vals: |
|
74 |
attributes_to_send[k] = v |
|
75 |
i = i + 1 |
|
76 |
request.session['attributes_to_send'] = attributes_to_send |
|
77 |
if 'next' in request.POST: |
|
78 |
next = request.POST['next'] |
|
79 |
if 'accept' in request.POST: |
|
80 |
next = next + '&consent_attribute_answer=accepted' |
|
81 |
else: |
|
82 |
next = next + '&consent_attribute_answer=refused' |
|
83 |
return HttpResponseRedirect(next) |
src/authentic2/idp/management/commands/cleanup.py | ||
---|---|---|
1 |
import warnings |
|
2 | ||
3 |
from authentic2.idp.management.commands import cleanupauthentic |
|
4 | ||
5 | ||
6 |
class Command(cleanupauthentic.Command): |
|
7 |
def handle_noargs(self, **options): |
|
8 |
warnings.warn( |
|
9 |
"The `cleanup` command has been deprecated in favor of `cleanupauthentic`.", |
|
10 |
PendingDeprecationWarning) |
|
11 |
super(Command, self).handle_noargs(**options) |
src/authentic2/idp/management/commands/cleanupauthentic.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 | 19 |
from django.apps import apps |
4 | 20 |
from django.core.management.base import BaseCommand |
5 | 21 | |
22 |
logger = logging.getLogger(__name__) |
|
23 | ||
6 | 24 | |
7 | 25 |
class Command(BaseCommand): |
8 | 26 |
help = 'Clean expired models of authentic2.' |
9 | 27 | |
10 | 28 |
def handle(self, **options): |
11 |
log = logging.getLogger(__name__) |
|
12 | 29 |
for app in apps.get_app_configs(): |
13 | 30 |
for model in app.get_models(): |
14 | 31 |
# only models from authentic2 |
15 | 32 |
if model.__module__.startswith('authentic2'): |
16 | 33 |
try: |
17 | 34 |
self.cleanup_model(model) |
18 |
except: |
|
19 |
log.exception('cleanup of model %s failed', model) |
|
35 |
except Exception:
|
|
36 |
logger.exception('cleanup of model %s failed', model)
|
|
20 | 37 | |
21 | 38 |
def cleanup_model(self, model): |
22 | 39 |
manager = getattr(model, 'objects', None) |
src/authentic2/idp/middleware.py | ||
---|---|---|
1 |
import traceback |
|
2 | ||
3 |
from django.conf import settings |
|
4 | ||
5 |
class DebugMiddleware: |
|
6 |
def process_exception(self, request, exception): |
|
7 |
if getattr(settings, 'DEBUG', False): |
|
8 |
traceback.print_exc() |
src/authentic2/idp/saml/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf import settings |
2 |
from django.utils.translation import ugettext_lazy as _ |
|
3 | 18 |
from django.core.checks import register, Warning, Tags |
4 | 19 |
from django.apps import AppConfig |
5 | 20 | |
... | ... | |
44 | 59 |
from . import app_settings |
45 | 60 |
errors = [] |
46 | 61 | |
47 |
if not settings.DEBUG and app_settings.ENABLE and \ |
|
48 |
(app_settings.is_default('SIGNATURE_PUBLIC_KEY') or |
|
49 |
app_settings.is_default('SIGNATURE_PRIVATE_KEY')): |
|
62 |
if (not settings.DEBUG |
|
63 |
and app_settings.ENABLE |
|
64 |
and (app_settings.is_default('SIGNATURE_PUBLIC_KEY') |
|
65 |
or app_settings.is_default('SIGNATURE_PRIVATE_KEY'))): |
|
50 | 66 |
errors.append( |
51 | 67 |
Warning( |
52 | 68 |
'You should not use default SAML keys in production', |
... | ... | |
57 | 73 |
) |
58 | 74 |
return errors |
59 | 75 | |
60 |
check_authentic2_config = register(Tags.security, |
|
61 |
deploy=True)(check_authentic2_config) |
|
76 |
check_authentic2_config = register(Tags.security, deploy=True)(check_authentic2_config) |
src/authentic2/idp/saml/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 | ||
19 | ||
1 | 20 |
class AppSettings(object): |
2 | 21 |
__DEFAULTS = dict( |
3 |
ENABLE=False,
|
|
4 |
METADATA_OPTIONS={},
|
|
5 |
SECONDS_TOLERANCE=60,
|
|
6 |
AUTHN_CONTEXT_FROM_SESSION=True,
|
|
7 |
SIGNATURE_PUBLIC_KEY = '''-----BEGIN CERTIFICATE-----
|
|
22 |
ENABLE=False, |
|
23 |
METADATA_OPTIONS={}, |
|
24 |
SECONDS_TOLERANCE=60, |
|
25 |
AUTHN_CONTEXT_FROM_SESSION=True, |
|
26 |
SIGNATURE_PUBLIC_KEY='''-----BEGIN CERTIFICATE-----
|
|
8 | 27 |
MIIDIzCCAgugAwIBAgIJANUBoick1pDpMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV |
9 | 28 |
BAoTCkVudHJvdXZlcnQwHhcNMTAxMjE0MTUzMzAyWhcNMTEwMTEzMTUzMzAyWjAV |
10 | 29 |
MRMwEQYDVQQKEwpFbnRyb3V2ZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB |
... | ... | |
23 | 42 |
JumlBc6IViKhJeo1wiBBrVRIIkKKevHKQzteK8pWm9CYWculxT26TZ4VWzGbo06j |
24 | 43 |
o2zbumirrLLqnt1gmBDvDvlOwC/zAAyL4chbz66eQHTiIYZZvYgy |
25 | 44 |
-----END CERTIFICATE-----''', |
26 |
SIGNATURE_PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
|
|
45 |
SIGNATURE_PRIVATE_KEY='''-----BEGIN RSA PRIVATE KEY-----
|
|
27 | 46 |
MIIEpAIBAAKCAQEAvxFkfPdndlGgQPDZgFGXbrNAc/79PULZBuNdWFHDD9P5hNhZ |
28 | 47 |
n9Kqm4Cp06Pe/A6u+g5wLnYvbZQcFCgfQAEzziJtb3J55OOlB7iMEI/T2AX2WzrU |
29 | 48 |
H8QT8NGhABONKU2Gg4XiyeXNhH5R7zdHlUwcWq3ZwNbtbY0TVc+n665EbrfV/59x |
... | ... | |
50 | 69 |
wRiVcNacaP+BivkrMjr4BlsUM6yH4MOBsNhLURiiCL+tLJV7U0DWlCse/doWij4U |
51 | 70 |
TKX6tp6oI+7MIJE6ySZ0cBqOiydAkBePZhu57j6ToBkTa0dbHjn1WA== |
52 | 71 |
-----END RSA PRIVATE KEY-----''', |
53 |
ADD_CERTIFICATE_TO_KEY_INFO=True,
|
|
54 |
SIGNATURE_METHOD='RSA-SHA256',
|
|
72 |
ADD_CERTIFICATE_TO_KEY_INFO=True, |
|
73 |
SIGNATURE_METHOD='RSA-SHA256', |
|
55 | 74 |
) |
56 | 75 | |
57 | 76 |
def __init__(self, prefix): |
... | ... | |
67 | 86 |
@property |
68 | 87 |
def ENABLE(self): |
69 | 88 |
return self._setting_with_prefix('ENABLE', |
70 |
self._setting('IDP_SAML2', |
|
71 |
self.__DEFAULTS['ENABLE'])) |
|
89 |
self._setting('IDP_SAML2',
|
|
90 |
self.__DEFAULTS['ENABLE']))
|
|
72 | 91 | |
73 | 92 |
@property |
74 | 93 |
def SIGNATURE_PUBLIC_KEY(self): |
75 | 94 |
return self._setting_with_prefix('SIGNATURE_PUBLIC_KEY', |
76 |
self._setting('SAML_SIGNATURE_PUBLIC_KEY', |
|
77 |
self.__DEFAULTS['SIGNATURE_PUBLIC_KEY'])) |
|
95 |
self._setting('SAML_SIGNATURE_PUBLIC_KEY',
|
|
96 |
self.__DEFAULTS['SIGNATURE_PUBLIC_KEY']))
|
|
78 | 97 | |
79 | 98 |
@property |
80 | 99 |
def SIGNATURE_PRIVATE_KEY(self): |
81 | 100 |
return self._setting_with_prefix('SIGNATURE_PRIVATE_KEY', |
82 |
self._setting('SAML_SIGNATURE_PRIVATE_KEY', |
|
83 |
self.__DEFAULTS['SIGNATURE_PRIVATE_KEY'])) |
|
101 |
self._setting('SAML_SIGNATURE_PRIVATE_KEY',
|
|
102 |
self.__DEFAULTS['SIGNATURE_PRIVATE_KEY']))
|
|
84 | 103 | |
85 | 104 |
@property |
86 | 105 |
def AUTHN_CONTEXT_FROM_SESSION(self): |
87 | 106 |
return self._setting_with_prefix('AUTHN_CONTEXT_FROM_SESSION', |
88 |
self._setting('IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION', |
|
89 |
self.__DEFAULTS['AUTHN_CONTEXT_FROM_SESSION'])) |
|
107 |
self._setting('IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION',
|
|
108 |
self.__DEFAULTS['AUTHN_CONTEXT_FROM_SESSION']))
|
|
90 | 109 | |
91 | 110 |
def is_default(self, name): |
92 | 111 |
return getattr(self, name) == self.__DEFAULTS[name] |
... | ... | |
96 | 115 |
raise AttributeError(name) |
97 | 116 |
return self._setting_with_prefix(name, self.__DEFAULTS[name]) |
98 | 117 | |
99 | ||
100 | 118 |
# Ugly? Guido recommends this himself ... |
101 | 119 |
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html |
102 |
import sys |
|
103 | 120 |
app_settings = AppSettings('A2_IDP_SAML2_') |
104 | 121 |
app_settings.__name__ = __name__ |
105 | 122 |
sys.modules[__name__] = app_settings |
src/authentic2/idp/saml/backend.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
import operator |
3 | 19 |
import random |
src/authentic2/idp/saml/common.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 |
from django.contrib.auth import REDIRECT_FIELD_NAME, SESSION_KEY
|
|
19 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
|
4 | 20 |
from django.utils.http import urlencode |
5 | 21 |
from importlib import import_module |
6 | 22 |
from django.conf import settings |
7 | 23 |
from django.http import HttpResponseRedirect |
8 | 24 | |
9 |
def redirect_to_login(next, login_url=None, |
|
10 |
redirect_field_name=REDIRECT_FIELD_NAME, other_keys = {}):
|
|
25 | ||
26 |
def redirect_to_login(next_url, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME, other_keys={}):
|
|
11 | 27 |
"Redirects the user to the login page, passing the given 'next' page" |
12 | 28 |
if not login_url: |
13 | 29 |
login_url = settings.LOGIN_URL |
14 |
data = { redirect_field_name: next }
|
|
30 |
data = {redirect_field_name: next_url}
|
|
15 | 31 |
for k, v in other_keys.items(): |
16 | 32 |
data[k] = v |
17 | 33 |
return HttpResponseRedirect('%s?%s' % (login_url, urlencode(data))) |
18 | 34 | |
35 | ||
19 | 36 |
def kill_django_sessions(session_key): |
20 | 37 |
engine = import_module(settings.SESSION_ENGINE) |
21 | 38 |
try: |
src/authentic2/idp/saml/saml2_endpoints.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
"""SAML2.0 IdP implementation |
2 | 18 | |
3 | 19 |
It contains endpoints to receive: |
... | ... | |
26 | 42 | |
27 | 43 |
from authentic2.compat_lasso import lasso |
28 | 44 |
from django.core.urlresolvers import reverse |
45 |
from django.contrib.auth import get_user_model |
|
29 | 46 |
from django.contrib.auth.decorators import login_required |
30 | 47 |
from django.core.exceptions import ObjectDoesNotExist |
31 | 48 |
from django.http import HttpResponse, HttpResponseRedirect, \ |
... | ... | |
44 | 61 |
from django.contrib import messages |
45 | 62 | |
46 | 63 | |
47 |
from authentic2.compat import get_user_model |
|
48 | 64 |
import authentic2.views as a2_views |
49 |
from authentic2.saml.models import (LibertyArtifact,
|
|
50 |
LibertySession, LibertyFederation,
|
|
51 |
nameid2kwargs, saml2_urn_to_nidformat,
|
|
52 |
nidformat_to_saml2_urn, save_key_values, get_and_delete_key_values,
|
|
53 |
LibertyProvider, LibertyServiceProvider, SAMLAttribute, NAME_ID_FORMATS)
|
|
65 |
from authentic2.saml.models import ( |
|
66 |
LibertyArtifact, LibertySession, LibertyFederation, nameid2kwargs,
|
|
67 |
saml2_urn_to_nidformat, nidformat_to_saml2_urn, save_key_values,
|
|
68 |
get_and_delete_key_values, LibertyProvider, LibertyServiceProvider,
|
|
69 |
SAMLAttribute, NAME_ID_FORMATS) |
|
54 | 70 |
from authentic2.saml.common import redirect_next, asynchronous_bindings, \ |
55 | 71 |
soap_bindings, load_provider, get_saml2_request_message, \ |
56 | 72 |
error_page, set_saml2_response_responder_status_code, \ |
... | ... | |
74 | 90 | |
75 | 91 |
from authentic2.idp import signals as idp_signals |
76 | 92 | |
77 |
from authentic2.utils import (make_url, get_backends as get_idp_backends, |
|
78 |
get_username, login_require, find_authentication_event, datetime_to_xs_datetime) |
|
93 |
from authentic2.utils import ( |
|
94 |
make_url, get_backends as get_idp_backends, get_username, login_require, |
|
95 |
find_authentication_event, datetime_to_xs_datetime) |
|
79 | 96 |
from authentic2 import utils |
80 | 97 |
from authentic2.attributes_ng.engine import get_attributes |
81 | 98 |
from authentic2 import hooks |
... | ... | |
83 | 100 |
from . import app_settings |
84 | 101 | |
85 | 102 | |
103 |
User = get_user_model() |
|
104 | ||
86 | 105 |
logger = logging.getLogger(__name__) |
87 | 106 | |
88 | 107 | |
89 | 108 |
def get_nonce(): |
90 |
alphabet = string.ascii_letters+string.digits
|
|
91 |
return '_'+''.join(random.SystemRandom().choice(alphabet) for i in xrange(20))
|
|
109 |
alphabet = string.ascii_letters + string.digits
|
|
110 |
return '_' + ''.join(random.SystemRandom().choice(alphabet) for i in range(20))
|
|
92 | 111 | |
93 | 112 |
metadata_map = ( |
94 |
(saml2utils.Saml2Metadata.SINGLE_SIGN_ON_SERVICE, |
|
95 |
asynchronous_bindings, '/sso'), |
|
96 |
(saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, |
|
97 |
asynchronous_bindings, '/slo', '/slo_return'), |
|
98 |
(saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, |
|
99 |
soap_bindings, '/slo/soap'), |
|
100 |
(saml2utils.Saml2Metadata.ARTIFACT_RESOLUTION_SERVICE, |
|
101 |
lasso.SAML2_METADATA_BINDING_SOAP, '/artifact') |
|
113 |
(saml2utils.Saml2Metadata.SINGLE_SIGN_ON_SERVICE, asynchronous_bindings, '/sso'), |
|
114 |
(saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, asynchronous_bindings, '/slo', '/slo_return'), |
|
115 |
(saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, soap_bindings, '/slo/soap'), |
|
116 |
(saml2utils.Saml2Metadata.ARTIFACT_RESOLUTION_SERVICE, lasso.SAML2_METADATA_BINDING_SOAP, '/artifact') |
|
102 | 117 |
) |
103 | 118 | |
119 | ||
104 | 120 |
def metadata(request): |
105 | 121 |
'''Endpoint to retrieve the metadata file''' |
106 |
return HttpResponse(get_metadata(request, request.path), |
|
107 |
content_type='text/xml') |
|
122 |
return HttpResponse(get_metadata(request, request.path), content_type='text/xml')
|
|
123 | ||
108 | 124 | |
109 | 125 |
def log_assert(func, exception_classes=(AssertionError,)): |
110 | 126 |
'''Convert assertion errors to warning logs and report them to the user |
... | ... | |
123 | 139 |
##### |
124 | 140 |
# SSO |
125 | 141 |
##### |
142 | ||
143 | ||
126 | 144 |
def register_new_saml2_session(request, login): |
127 | 145 |
'''Persist the newly created session for emitted assertion''' |
128 |
lib_session = LibertySession(provider_id=login.remoteProviderId, |
|
129 |
saml2_assertion=login.assertion, |
|
130 |
django_session_key=request.session.session_key) |
|
146 |
lib_session = LibertySession( |
|
147 |
provider_id=login.remoteProviderId, |
|
148 |
saml2_assertion=login.assertion, |
|
149 |
django_session_key=request.session.session_key) |
|
131 | 150 |
lib_session.save() |
132 | 151 | |
133 | 152 | |
... | ... | |
158 | 177 |
# Generate the transient identifier from the session key, to fix it for |
159 | 178 |
# a session duration, without that logout is broken as you can send |
160 | 179 |
# many session_index in a logout request but only one NameID |
161 |
keys = ''.join([request.session.session_key, provider_id, |
|
162 |
settings.SECRET_KEY]) |
|
180 |
keys = ''.join([request.session.session_key, provider_id, settings.SECRET_KEY]) |
|
163 | 181 |
transient_id_content = '_' + hashlib.sha1(keys).hexdigest().upper() |
164 | 182 |
assertion.subject.nameID.content = transient_id_content |
165 | 183 |
if nid_format == 'email': |
... | ... | |
172 | 190 |
assertion.subject.nameID.content = request.user.uuid |
173 | 191 |
if nid_format == 'edupersontargetedid': |
174 | 192 |
assertion.subject.nameID.format = NAME_ID_FORMATS[nid_format]['samlv2'] |
175 |
keys = ''.join([get_username(request.user), |
|
176 |
provider_id, settings.SECRET_KEY]) |
|
193 |
keys = ''.join([get_username(request.user), provider_id, settings.SECRET_KEY]) |
|
177 | 194 |
edu_person_targeted_id = '_' + hashlib.sha1(keys).hexdigest().upper() |
178 | 195 |
assertion.subject.nameID.content = edu_person_targeted_id |
179 | 196 |
attribute_definition = ('urn:oid:1.3.6.1.4.1.5923.1.1.1.10', |
180 |
lasso.SAML2_ATTRIBUTE_NAME_FORMAT_URI, 'eduPersonTargetedID') |
|
197 |
lasso.SAML2_ATTRIBUTE_NAME_FORMAT_URI, |
|
198 |
'eduPersonTargetedID') |
|
181 | 199 |
value = assertion.subject.nameID.exportToXml() |
182 | 200 |
value = ctree.fromstring(value) |
183 |
saml2_add_attribute_values(assertion, |
|
184 |
{ attribute_definition: [ value ]}) |
|
201 |
saml2_add_attribute_values(assertion, {attribute_definition: [value]}) |
|
185 | 202 |
logger.debug('adding an eduPersonTargetedID attribute with value %s', edu_person_targeted_id) |
186 | 203 |
assertion.subject.nameID.format = NAME_ID_FORMATS[nid_format]['samlv2'] |
187 | 204 | |
205 | ||
188 | 206 |
def get_attribute_definitions(provider): |
189 | 207 |
'''Query all attribute definitions for a providers''' |
190 |
qs = SAMLAttribute.objects.for_generic_object(provider) \ |
|
191 |
.filter(enabled=True) |
|
208 |
qs = SAMLAttribute.objects.for_generic_object(provider).filter(enabled=True) |
|
192 | 209 |
sp_options_policy = get_sp_options_policy(provider) |
193 | 210 |
if sp_options_policy: |
194 |
qs |= SAMLAttribute.objects.for_generic_object(sp_options_policy) \ |
|
195 |
.filter(enabled=True) |
|
211 |
qs |= SAMLAttribute.objects.for_generic_object(sp_options_policy).filter(enabled=True) |
|
196 | 212 |
return qs.distinct() |
197 | 213 | |
214 | ||
198 | 215 |
def add_attributes(request, assertion, provider): |
199 | 216 |
qs = get_attribute_definitions(provider) |
200 | 217 |
wanted_attributes = [definition.attribute_name for definition in qs] |
... | ... | |
349 | 366 |
elif how.startswith('oath-totp'): |
350 | 367 |
authn_context = lasso.SAML2_AUTHN_CONTEXT_TIME_SYNC_TOKEN |
351 | 368 |
else: |
352 |
raise NotImplementedError('Unknown authentication method %s', |
|
353 |
how) |
|
369 |
raise NotImplementedError('Unknown authentication method %s', how) |
|
354 | 370 |
except ObjectDoesNotExist: |
355 | 371 |
# TODO: previous session over secure transport (ssl) ? |
356 | 372 |
authn_context = lasso.SAML2_AUTHN_CONTEXT_PREVIOUS_SESSION |
357 | 373 |
logger.debug('authn_context is %s', authn_context) |
358 |
login.buildAssertion(authn_context, |
|
359 |
now.isoformat() + 'Z', |
|
360 |
'unused', # reauthenticateOnOrAfter is only for ID-FF 1.2 |
|
361 |
notBefore.isoformat() + 'Z', |
|
362 |
notOnOrAfter.isoformat() + 'Z') |
|
374 |
login.buildAssertion( |
|
375 |
authn_context, |
|
376 |
now.isoformat() + 'Z', |
|
377 |
'unused', # reauthenticateOnOrAfter is only for ID-FF 1.2 |
|
378 |
notBefore.isoformat() + 'Z', |
|
379 |
notOnOrAfter.isoformat() + 'Z') |
|
363 | 380 |
assertion = login.assertion |
364 | 381 |
assertion.conditions.notOnOrAfter = notOnOrAfter.isoformat() + 'Z' |
365 | 382 |
# Set SessionNotOnOrAfter to expiry date of the current session, so we are sure no session on |
... | ... | |
367 | 384 |
expiry_date = request.session.get_expiry_date() |
368 | 385 |
assertion.authnStatement[0].sessionNotOnOrAfter = datetime_to_xs_datetime(expiry_date) |
369 | 386 |
logger.debug('assertion building in progress %s', assertion.dump()) |
370 |
fill_assertion(request, login.request, assertion, login.remoteProviderId, |
|
371 |
nid_format) |
|
387 |
fill_assertion(request, login.request, assertion, login.remoteProviderId, nid_format) |
|
372 | 388 |
# Save federation and new session |
373 | 389 |
if nid_format == 'persistent': |
374 | 390 |
logger.debug('nameID persistent, get or create federation') |
... | ... | |
379 | 395 |
kwargs['name_id_qualifier'] = AUTHENTIC_SAME_ID_SENTINEL |
380 | 396 |
if kwargs.get('name_id_sp_name_qualifier') == login.remoteProviderId: |
381 | 397 |
kwargs['name_id_sp_name_qualifier'] = AUTHENTIC_SAME_ID_SENTINEL |
382 |
service_provider = LibertyServiceProvider.objects \
|
|
383 |
.get(liberty_provider__entity_id=login.remoteProviderId)
|
|
398 |
service_provider = LibertyServiceProvider.objects.get(
|
|
399 |
liberty_provider__entity_id=login.remoteProviderId) |
|
384 | 400 |
federation, new = LibertyFederation.objects.get_or_create( |
385 |
sp=service_provider, |
|
386 |
user=request.user, **kwargs) |
|
401 |
sp=service_provider, |
|
402 |
user=request.user, |
|
403 |
**kwargs) |
|
387 | 404 |
if new: |
388 | 405 |
logger.debug('nameID persistent, new federation') |
389 | 406 |
federation.save() |
... | ... | |
396 | 413 |
kwargs['entity_id'] = login.remoteProviderId |
397 | 414 |
kwargs['user'] = request.user |
398 | 415 |
logger.debug(u'sending nameID %(name_id_format)r: %(name_id_content)r to ' |
399 |
u'%(entity_id)s for user %(user)s' % kwargs) |
|
400 | ||
416 |
u'%(entity_id)s for user %(user)s' % kwargs) |
|
401 | 417 |
register_new_saml2_session(request, login) |
402 | 418 |
return kwargs['name_id_content'] |
403 | 419 | |
... | ... | |
436 | 452 |
logger.warning( |
437 | 453 |
'invalid message for WebSSO profile with HTTP-Redirect binding: ' |
438 | 454 |
'%r exception: %s', message, e, extra={'request': request}) |
439 |
return HttpResponseBadRequest(_("SAMLv2 Single Sign On: "
|
|
440 |
"invalid message for WebSSO profile with HTTP-Redirect "
|
|
441 |
"binding: %r") % message, content_type='text/plain') |
|
455 |
return HttpResponseBadRequest( |
|
456 |
_("SAMLv2 Single Sign On: invalid message for WebSSO profile with HTTP-Redirect "
|
|
457 |
"binding: %r") % message, content_type='text/plain')
|
|
442 | 458 |
except lasso.ProfileInvalidProtocolprofileError: |
443 | 459 |
log_info_authn_request_details(login) |
444 |
message = _("SAMLv2 Single Sign On: the request cannot be " |
|
460 |
message = _( |
|
461 |
"SAMLv2 Single Sign On: the request cannot be " |
|
445 | 462 |
"answered because no valid protocol binding could be found") |
446 |
logger.warning('the request cannot be answered because no '
|
|
447 |
'valid protocol binding could be found') |
|
463 |
logger.warning( |
|
464 |
'the request cannot be answered because no valid protocol binding could be found')
|
|
448 | 465 |
return HttpResponseBadRequest(message, content_type='text/plain') |
449 | 466 |
except lasso.ProviderMissingPublicKeyError as e: |
450 | 467 |
log_info_authn_request_details(login) |
... | ... | |
462 | 479 |
log_info_authn_request_details(login) |
463 | 480 |
provider_id = login.remoteProviderId |
464 | 481 |
logger.debug('loading provider %s' % provider_id) |
465 |
provider_loaded = load_provider(request, provider_id, |
|
466 |
server=login.server, autoload=True) |
|
482 |
provider_loaded = load_provider(request, provider_id, server=login.server, autoload=True) |
|
467 | 483 |
if not provider_loaded: |
468 | 484 |
add_url = reverse('admin:saml_libertyprovider_add_from_url') |
469 |
add_url += '?' + urlencode({ 'entity_id': provider_id }) |
|
470 |
return render(request, |
|
471 |
'idp/saml/unknown_provider.html', |
|
472 |
{ 'entity_id': provider_id, |
|
473 |
'add_url': add_url, |
|
474 |
}) |
|
485 |
add_url += '?' + urlencode({'entity_id': provider_id}) |
|
486 |
return render( |
|
487 |
request, |
|
488 |
'idp/saml/unknown_provider.html', |
|
489 |
{ |
|
490 |
'entity_id': provider_id, |
|
491 |
'add_url': add_url, |
|
492 |
}) |
|
475 | 493 |
else: |
476 | 494 |
policy = get_sp_options_policy(provider_loaded) |
477 | 495 |
if not policy: |
478 |
return error_page(request, _('sso: No SP policy defined'), |
|
496 |
return error_page( |
|
497 |
request, |
|
498 |
_('sso: No SP policy defined'), |
|
479 | 499 |
logger=logger, warning=True) |
480 | 500 |
logger.debug('provider %s loaded with success', provider_id) |
481 | 501 |
if policy.authn_request_signed: |
... | ... | |
487 | 507 | |
488 | 508 |
if signed and not check_destination(request, login.request): |
489 | 509 |
logger.warning('wrong or absent destination') |
490 |
return return_login_error(request, login, |
|
491 |
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION) |
|
510 |
return return_login_error( |
|
511 |
request, login, |
|
512 |
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION) |
|
492 | 513 |
# Check NameIDPolicy or force the NameIDPolicy |
493 | 514 |
name_id_policy = login.request.nameIdPolicy |
494 |
if name_id_policy and \ |
|
495 |
name_id_policy.format and \ |
|
496 |
name_id_policy.format != \ |
|
497 |
lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: |
|
515 |
if (name_id_policy |
|
516 |
and name_id_policy.format |
|
517 |
and name_id_policy.format != lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED): |
|
498 | 518 |
logger.debug('nameID policy is %s', name_id_policy.dump()) |
499 |
nid_format = saml2_urn_to_nidformat(name_id_policy.format, |
|
500 |
accepted=policy.accepted_name_id_format) |
|
519 |
nid_format = saml2_urn_to_nidformat(name_id_policy.format, accepted=policy.accepted_name_id_format) |
|
501 | 520 |
logger.debug('nameID format %s', nid_format) |
502 | 521 |
default_nid_format = policy.default_name_id_format |
503 | 522 |
logger.debug('default nameID format %s', default_nid_format) |
... | ... | |
505 | 524 |
logger.debug('nameID format accepted %s', accepted_nid_format) |
506 | 525 |
if (not nid_format or nid_format not in accepted_nid_format) and \ |
507 | 526 |
default_nid_format != nid_format: |
508 |
set_saml2_response_responder_status_code(login.response, |
|
509 |
lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY) |
|
527 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY) |
|
510 | 528 |
logger.warning('NameID format required is not accepted') |
511 | 529 |
return finish_sso(request, login) |
512 | 530 |
else: |
... | ... | |
576 | 594 |
logger.warning('nonce not found') |
577 | 595 |
return HttpResponseBadRequest() |
578 | 596 |
try: |
579 |
login_dump, consent_obtained, nid_format = \ |
|
580 |
get_and_delete_key_values(nonce) |
|
597 |
login_dump, consent_obtained, nid_format = get_and_delete_key_values(nonce) |
|
581 | 598 |
except KeyError: |
582 | 599 |
messages.warning(request, N_('request has expired')) |
583 | 600 |
return utils.redirect(request, 'auth_homepage') |
584 | 601 |
server = create_server(request) |
585 | 602 |
# Work Around for lasso < 2.3.6 |
586 |
login_dump = login_dump.replace('<Login ', '<lasso:Login ') \ |
|
587 |
.replace('</Login>', '</lasso:Login>') |
|
603 |
login_dump = login_dump.replace('<Login ', '<lasso:Login ').replace('</Login>', '</lasso:Login>') |
|
588 | 604 |
login = lasso.Login.newFromDump(server, login_dump) |
589 | 605 |
logger.debug('login newFromDump done') |
590 | 606 |
if not login: |
591 |
return error_page(request, _('continue_sso: error loading login'), |
|
592 |
logger=logger) |
|
593 |
if not load_provider(request, login.remoteProviderId, server=login.server, |
|
594 |
autoload=True): |
|
595 |
return error_page(request, _('continue_sso: unknown provider %s') \ |
|
596 |
% login.remoteProviderId, logger=logger) |
|
607 |
return error_page(request, _('continue_sso: error loading login'), logger=logger) |
|
608 |
if not load_provider(request, login.remoteProviderId, server=login.server, autoload=True): |
|
609 |
return error_page(request, _('continue_sso: unknown provider %s') % login.remoteProviderId, logger=logger) |
|
597 | 610 |
if 'cancel' in request.GET: |
598 | 611 |
logger.debug('login canceled') |
599 |
set_saml2_response_responder_status_code(login.response, |
|
600 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
612 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
601 | 613 |
return finish_sso(request, login) |
602 | 614 |
if consent_answer == 'refused': |
603 | 615 |
logger.debug('consent answer treatment, the user refused, return request denied to the requester') |
604 |
set_saml2_response_responder_status_code(login.response, |
|
605 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
616 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
606 | 617 |
return finish_sso(request, login) |
607 | 618 |
if consent_answer == 'accepted': |
608 | 619 |
logger.debug('consent answer treatment, the user accepted, continue') |
609 | 620 |
consent_obtained = True |
610 |
return sso_after_process_request(request, login, |
|
611 |
consent_obtained=consent_obtained, |
|
612 |
consent_attribute_answer=consent_attribute_answer, |
|
613 |
nid_format=nid_format) |
|
621 |
return sso_after_process_request( |
|
622 |
request, |
|
623 |
login, |
|
624 |
consent_obtained=consent_obtained, |
|
625 |
consent_attribute_answer=consent_attribute_answer, |
|
626 |
nid_format=nid_format) |
|
614 | 627 | |
615 | 628 | |
616 | 629 |
def needs_persistence(nid_format): |
... | ... | |
618 | 631 | |
619 | 632 | |
620 | 633 |
def sso_after_process_request(request, login, consent_obtained=False, |
621 |
consent_attribute_answer=False, user=None, |
|
622 |
nid_format='transient', return_profile=False): |
|
634 |
consent_attribute_answer=False, user=None,
|
|
635 |
nid_format='transient', return_profile=False):
|
|
623 | 636 |
"""Common path for sso and idp_initiated_sso. |
624 | 637 | |
625 | 638 |
consent_obtained: whether the user has given his consent to this |
... | ... | |
648 | 661 |
# No user is authenticated and passive is True, deny request |
649 | 662 |
if passive and user.is_anonymous(): |
650 | 663 |
logger.debug('no user connected and passive request, returning NoPassive') |
651 |
set_saml2_response_responder_status_code(login.response, |
|
652 |
lasso.SAML2_STATUS_CODE_NO_PASSIVE) |
|
664 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_NO_PASSIVE) |
|
653 | 665 |
return finish_sso(request, login) |
654 | 666 | |
655 | 667 |
service.authorize(request.user) |
656 | 668 | |
657 | 669 |
hooks.call_hooks('event', name='sso-request', idp='saml2', service=service) |
658 | 670 | |
659 |
#Do not ask consent for federation if a transient nameID is provided |
|
671 |
# Do not ask consent for federation if a transient nameID is provided
|
|
660 | 672 |
transient = False |
661 | 673 |
if nid_format == 'transient': |
662 | 674 |
transient = True |
663 | 675 | |
664 |
decisions = idp_signals.authorize_service.send(sender=None, |
|
665 |
request=request, user=request.user, audience=login.remoteProviderId, |
|
666 |
attributes={}) |
|
676 |
decisions = idp_signals.authorize_service.send( |
|
677 |
sender=None, |
|
678 |
request=request, user=request.user, |
|
679 |
audience=login.remoteProviderId, |
|
680 |
attributes={}) |
|
667 | 681 |
logger.debug('signal authorize_service sent') |
668 | 682 | |
669 | 683 |
# You don't dream. By default, access granted. |
670 | 684 |
# We catch denied decisions i.e. dic['authz'] = False |
671 | 685 |
access_granted = True |
672 | 686 |
for decision in decisions: |
673 |
logger.debug('authorize_service connected ' |
|
674 |
'to function %s' % decision[0].__name__) |
|
687 |
logger.debug('authorize_service connected to function %s' % decision[0].__name__) |
|
675 | 688 |
dic = decision[1] |
676 | 689 |
if dic and 'authz' in dic: |
677 | 690 |
logger.debug('decision is %s', dic['authz']) |
... | ... | |
685 | 698 | |
686 | 699 |
if not access_granted: |
687 | 700 |
logger.debug('access denied, return answer to the requester') |
688 |
set_saml2_response_responder_status_code(login.response, |
|
689 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED, |
|
690 |
msg=six.text_type(dic['message'])) |
|
701 |
set_saml2_response_responder_status_code( |
|
702 |
login.response, |
|
703 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED, |
|
704 |
msg=six.text_type(dic['message'])) |
|
691 | 705 |
return finish_sso(request, login) |
692 | 706 | |
693 |
provider = load_provider(request, login.remoteProviderId, |
|
694 |
server=login.server) |
|
707 |
provider = load_provider(request, login.remoteProviderId, server=login.server) |
|
695 | 708 |
if not provider: |
696 |
return error_page(request, |
|
709 |
return error_page( |
|
710 |
request, |
|
697 | 711 |
_('Provider %s is unknown') % login.remoteProviderId, |
698 | 712 |
logger=logger) |
699 | 713 |
saml_policy = get_sp_options_policy(provider) |
700 | 714 |
if not saml_policy: |
701 |
return error_page(request, _('No service provider policy defined'), |
|
702 |
logger=logger) |
|
715 |
return error_page(request, _('No service provider policy defined'), logger=logger) |
|
703 | 716 | |
704 | 717 |
'''User consent for federation management |
705 | 718 | |
... | ... | |
750 | 763 |
consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:unavailable' |
751 | 764 | |
752 | 765 |
if not consent_obtained and not transient: |
753 |
consent_obtained = \ |
|
754 |
not saml_policy.ask_user_consent |
|
766 |
consent_obtained = not saml_policy.ask_user_consent |
|
755 | 767 |
logger.debug('the policy says %s', consent_obtained) |
756 | 768 |
if consent_obtained: |
757 |
#The user consent is bypassed by the policy |
|
769 |
# The user consent is bypassed by the policy
|
|
758 | 770 |
consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:unspecified' |
759 | 771 | |
760 | 772 |
if needs_persistence(nid_format): |
761 | 773 |
try: |
762 | 774 |
LibertyFederation.objects.get( |
763 |
user=request.user,
|
|
764 |
sp__liberty_provider__entity_id=login.remoteProviderId)
|
|
775 |
user=request.user, |
|
776 |
sp__liberty_provider__entity_id=login.remoteProviderId) |
|
765 | 777 |
logger.debug('consent already given (existing federation) for %s', login.remoteProviderId) |
766 | 778 |
consent_obtained = True |
767 | 779 |
'''This is abusive since a federation may exist even if we have |
... | ... | |
773 | 785 |
if not consent_obtained and not transient: |
774 | 786 |
logger.debug('signal avoid_consent sent') |
775 | 787 |
avoid_consent = idp_signals.avoid_consent.send(sender=None, |
776 |
request=request, user=request.user, |
|
777 |
audience=login.remoteProviderId) |
|
788 |
request=request, |
|
789 |
user=request.user, |
|
790 |
audience=login.remoteProviderId) |
|
778 | 791 |
for c in avoid_consent: |
779 | 792 |
logger.debug('avoid_consent connected to function %s', c[0].__name__) |
780 | 793 |
if c[1] and 'avoid_consent' in c[1] and c[1]['avoid_consent']: |
781 | 794 |
logger.debug('avoid consent by signal') |
782 | 795 |
consent_obtained = True |
783 |
#The user consent is bypassed by the signal |
|
796 |
# The user consent is bypassed by the signal
|
|
784 | 797 |
consent_value = \ |
785 | 798 |
'urn:oasis:names:tc:SAML:2.0:consent:unspecified' |
786 | 799 | |
... | ... | |
797 | 810 |
logger.debug('validateRequestMsg %s', login.dump()) |
798 | 811 |
except lasso.LoginRequestDeniedError: |
799 | 812 |
logger.warning('access denied due to LoginRequestDeniedError') |
800 |
set_saml2_response_responder_status_code(login.response, |
|
801 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
813 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
802 | 814 |
return finish_sso(request, login, user=user) |
803 | 815 |
except lasso.LoginFederationNotFoundError: |
804 | 816 |
logger.warning('access denied due to LoginFederationNotFoundError') |
805 |
set_saml2_response_responder_status_code(login.response, |
|
806 |
lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
817 |
set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED) |
|
807 | 818 |
return finish_sso(request, login, user=user) |
808 | 819 | |
809 | 820 |
login.response.consent = consent_value |
... | ... | |
836 | 847 |
else: |
837 | 848 |
raise NotImplementedError() |
838 | 849 |
provider = LibertyProvider.objects.get(entity_id=login.remoteProviderId) |
839 |
return return_saml2_response(request, login, |
|
840 |
title=_('You are being redirected to "%s"') % provider.name) |
|
850 |
return return_saml2_response(request, login, title=_('You are being redirected to "%s"') % provider.name) |
|
841 | 851 | |
842 | 852 | |
843 | 853 |
def finish_sso(request, login, user=None, return_profile=False): |
... | ... | |
851 | 861 | |
852 | 862 |
def save_artifact(request, login): |
853 | 863 |
'''Remember an artifact message for later retrieving''' |
854 |
LibertyArtifact(artifact=login.artifact, |
|
855 |
content=login.artifactMessage.decode('utf-8'), |
|
856 |
provider_id=login.remoteProviderId).save() |
|
864 |
LibertyArtifact( |
|
865 |
artifact=login.artifact, |
|
866 |
content=login.artifactMessage.decode('utf-8'), |
|
867 |
provider_id=login.remoteProviderId).save() |
|
857 | 868 |
logger.debug('artifact saved') |
858 | 869 | |
859 | 870 | |
... | ... | |
880 | 891 |
try: |
881 | 892 |
login.processRequestMsg(soap_message) |
882 | 893 |
except (lasso.ProfileUnknownProviderError, lasso.ParamError): |
883 |
if not load_provider(request, login.remoteProviderId, |
|
884 |
server=login.server): |
|
894 |
if not load_provider(request, login.remoteProviderId, server=login.server): |
|
885 | 895 |
logger.warning('provider loading failure') |
886 | 896 |
try: |
887 | 897 |
login.processRequestMsg(soap_message) |
... | ... | |
890 | 900 |
else: |
891 | 901 |
logger.debug('reloading artifact') |
892 | 902 |
reload_artifact(login) |
893 |
except: |
|
903 |
except Exception:
|
|
894 | 904 |
logger.exception('resolve error') |
895 | 905 |
try: |
896 | 906 |
login.buildResponseMsg(None) |
897 | 907 |
logger.debug('resolve response %s' % login.msgBody) |
898 |
except: |
|
908 |
except Exception:
|
|
899 | 909 |
logger.exception('resolve error') |
900 |
return soap_fault(request, |
|
901 |
faultcode='soap:Server', |
|
902 |
faultstring='Internal Server Error') |
|
910 |
return soap_fault( |
|
911 |
request, |
|
912 |
faultcode='soap:Server', |
|
913 |
faultstring='Internal Server Error') |
|
903 | 914 |
logger.debug('treatment ended, return answer') |
904 | 915 |
return return_saml_soap_response(login) |
905 | 916 | |
... | ... | |
915 | 926 |
def idp_sso(request, provider_id=None, return_profile=False): |
916 | 927 |
'''Initiate an SSO toward provider_id without a prior AuthnRequest |
917 | 928 |
''' |
918 |
User = get_user_model() |
|
919 | 929 |
if not provider_id: |
920 | 930 |
provider_id = request.POST.get('provider_id') |
921 | 931 |
if not provider_id: |
922 |
return error_redirect(request, |
|
923 |
N_('missing provider identifier')) |
|
932 |
return error_redirect(request, N_('missing provider identifier')) |
|
924 | 933 |
logger.debug('start of an idp initiated sso toward %s', provider_id) |
925 | 934 |
server = create_server(request) |
926 | 935 |
login = lasso.Login(server) |
927 |
liberty_provider = load_provider(request, provider_id, |
|
928 |
server=login.server) |
|
936 |
liberty_provider = load_provider(request, provider_id, server=login.server) |
|
929 | 937 |
if not liberty_provider: |
930 | 938 |
return error_redirect(request, N_('provider %r is unknown'), provider_id) |
931 | 939 |
username = request.POST.get('username') |
932 | 940 |
if username: |
933 | 941 |
if not check_delegated_authentication_permission(request): |
934 |
return error_redirect(request, |
|
935 |
N_('%r tried to log as %r on %r but was forbidden'), |
|
936 |
request.user, username, provider_id) |
|
942 |
return error_redirect( |
|
943 |
request, |
|
944 |
N_('%r tried to log as %r on %r but was forbidden'), |
|
945 |
request.user, username, provider_id) |
|
937 | 946 |
try: |
938 | 947 |
user = User.objects.get_by_natural_key(username=username) |
939 | 948 |
except User.DoesNotExist: |
940 |
return error_redirect(request, |
|
941 |
N_('you cannot login as %r as it does not exist'), username) |
|
949 |
return error_redirect(request, N_('you cannot login as %r as it does not exist'), username) |
|
942 | 950 |
else: |
943 | 951 |
user = request.user |
944 | 952 |
policy = get_sp_options_policy(liberty_provider) |
945 | 953 |
# Control assertion consumer binding |
946 | 954 |
if not policy: |
947 |
return error_redirect(request, |
|
948 |
N_('missing service provider policy')) |
|
955 |
return error_redirect(request, N_('missing service provider policy')) |
|
949 | 956 |
nid_format = policy.default_name_id_format |
950 | 957 |
if needs_persistence(nid_format): |
951 | 958 |
load_federation(request, get_entity_id(request, reverse(metadata)), login, user) |
... | ... | |
958 | 965 |
elif binding == 'post': |
959 | 966 |
login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST |
960 | 967 |
else: |
961 |
return error_redirect(request, |
|
962 |
N_('unknown binding %r') % binding) |
|
968 |
return error_redirect(request, N_('unknown binding %r') % binding) |
|
963 | 969 |
# Control nid format policy |
964 | 970 |
# XXX: if a federation exist, we should use transient |
965 | 971 |
login.request.nameIdPolicy.format = nidformat_to_saml2_urn(nid_format) |
... | ... | |
970 | 976 |
logger.debug('binding %r', binding) |
971 | 977 |
logger.debug('authentication request initialized toward provider_id %r', provider_id) |
972 | 978 | |
973 |
return sso_after_process_request(request, login, |
|
974 |
consent_obtained=False, user=user,
|
|
975 |
nid_format=nid_format, return_profile=return_profile)
|
|
979 |
return sso_after_process_request(request, login, consent_obtained=False,
|
|
980 |
user=user, nid_format=nid_format,
|
|
981 |
return_profile=return_profile)
|
|
976 | 982 | |
977 | 983 | |
978 | 984 |
@never_cache |
... | ... | |
1007 | 1013 |
logger.warning('partial logout') |
1008 | 1014 |
logout.buildResponseMsg() |
1009 | 1015 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1010 |
return return_saml2_response(request, logout, |
|
1011 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1016 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1012 | 1017 | |
1013 | 1018 | |
1014 | 1019 |
def return_logout_error(request, logout, error): |
... | ... | |
1019 | 1024 |
logout.buildResponseMsg() |
1020 | 1025 |
logger.debug('returned an error message on logout: %s', error) |
1021 | 1026 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1022 |
return return_saml2_response(request, logout, |
|
1023 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1027 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1024 | 1028 | |
1025 | 1029 | |
1026 | 1030 |
def process_logout_request(request, message, binding): |
... | ... | |
1036 | 1040 |
except (lasso.ServerProviderNotFoundError, |
1037 | 1041 |
lasso.ProfileUnknownProviderError): |
1038 | 1042 |
logger.debug('loading provider %s', logout.remoteProviderId) |
1039 |
p = load_provider(request, logout.remoteProviderId, |
|
1040 |
server=logout.server) |
|
1043 |
p = load_provider(request, logout.remoteProviderId, server=logout.server) |
|
1041 | 1044 |
if not p: |
1042 | 1045 |
logger.warning('slo unknown provider %s', logout.remoteProviderId) |
1043 |
return logout, return_logout_error(request, logout, |
|
1044 |
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER) |
|
1046 |
return logout, return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER) |
|
1045 | 1047 |
policy = get_sp_options_policy(p) |
1046 | 1048 |
# we do not verify authn request, why verify logout requests... |
1047 | 1049 |
if not policy.authn_request_signed: |
... | ... | |
1049 | 1051 |
logout.processRequestMsg(message) |
1050 | 1052 |
except lasso.DsError: |
1051 | 1053 |
logger.warning('slo signature error') |
1052 |
return logout, return_logout_error(request, logout, |
|
1053 |
lasso.LIB_STATUS_CODE_INVALID_SIGNATURE) |
|
1054 |
return logout, return_logout_error(request, logout, lasso.LIB_STATUS_CODE_INVALID_SIGNATURE) |
|
1054 | 1055 |
except Exception as e: |
1055 | 1056 |
logger.warning('slo unknown error when processing a request: %s', e) |
1056 | 1057 |
return logout, HttpResponseBadRequest('Invalid logout request', content_type='text/plain') |
1057 | 1058 |
if binding != 'SOAP' and not check_destination(request, logout.request): |
1058 | 1059 |
logger.warning('slo wrong or absent destination') |
1059 |
return logout, return_logout_error(request, logout, |
|
1060 |
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION) |
|
1060 |
return logout, return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_DESTINATION) |
|
1061 | 1061 |
return logout, None |
1062 | 1062 | |
1063 | 1063 | |
... | ... | |
1085 | 1085 |
for backend in backends: |
1086 | 1086 |
ok = ok and backend.can_synchronous_logout(django_sessions_keys) |
1087 | 1087 |
if not ok: |
1088 |
return return_logout_error(request, logout, |
|
1089 |
lasso.SAML2_STATUS_CODE_UNSUPPORTED_BINDING) |
|
1088 |
return return_logout_error(request, logout, lasso.SAML2_STATUS_CODE_UNSUPPORTED_BINDING) |
|
1090 | 1089 |
logger.debug('treatments ended') |
1091 | 1090 |
return None |
1092 | 1091 | |
... | ... | |
1098 | 1097 |
Enumerate all emitted assertions for the given session, and for each |
1099 | 1098 |
provider only keep the more recent one. |
1100 | 1099 |
""" |
1101 |
lib_session1 = LibertySession.get_for_nameid_and_session_indexes( |
|
1102 |
issuer_id, provider_id, name_id, session_indexes) |
|
1100 |
lib_session1 = LibertySession.get_for_nameid_and_session_indexes(issuer_id, provider_id, name_id, session_indexes) |
|
1103 | 1101 |
django_session_keys = [s.django_session_key for s in lib_session1] |
1104 |
lib_session = LibertySession.objects.filter( |
|
1105 |
django_session_key__in=django_session_keys) |
|
1102 |
lib_session = LibertySession.objects.filter(django_session_key__in=django_session_keys) |
|
1106 | 1103 |
providers = set([s.provider_id for s in lib_session]) |
1107 | 1104 |
result = [] |
1108 | 1105 |
for provider in providers: |
... | ... | |
1118 | 1115 |
def build_session_dump(liberty_sessions): |
1119 | 1116 |
'''Build a session dump from a list of pairs |
1120 | 1117 |
(provider_id,assertion_content)''' |
1121 |
session = [u'<Session xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns="http://www.entrouvert.org/namespaces/lasso/0.0" Version="2">'] |
|
1118 |
session = [ |
|
1119 |
u'<Session xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' |
|
1120 |
u' xmlns="http://www.entrouvert.org/namespaces/lasso/0.0" Version="2">', |
|
1121 |
] |
|
1122 | 1122 |
for liberty_session in liberty_sessions: |
1123 | 1123 |
session.append(u'<NidAndSessionIndex ProviderID="{0.provider_id}" ' |
1124 | 1124 |
u'AssertionID="xxx" ' |
... | ... | |
1164 | 1164 |
LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1165 | 1165 |
except ObjectDoesNotExist: |
1166 | 1166 |
logger.warning('provider %r unknown', logout.remoteProviderId) |
1167 |
return return_logout_error(request, logout, |
|
1168 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1167 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1169 | 1168 |
policy = get_sp_options_policy(provider) |
1170 | 1169 |
if not policy: |
1171 | 1170 |
logger.warning('No policy found for %s', logout.remoteProviderId) |
1172 |
return return_logout_error(request, logout, |
|
1173 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1171 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1174 | 1172 |
if not policy.accept_slo: |
1175 | 1173 |
logger.warning('received slo from %s not authorized', logout.remoteProviderId) |
1176 |
return return_logout_error(request, logout, |
|
1177 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1174 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1178 | 1175 | |
1179 | 1176 |
'''Find all active sessions on SPs but the SP initiating the SLO''' |
1180 |
found, lib_sessions, django_session_keys = \ |
|
1181 |
get_only_last_session(logout.server.providerId, |
|
1182 |
logout.remoteProviderId, logout.request.nameId, |
|
1183 |
logout.request.sessionIndexes) |
|
1177 |
found, lib_sessions, django_session_keys = get_only_last_session( |
|
1178 |
logout.server.providerId, |
|
1179 |
logout.remoteProviderId, |
|
1180 |
logout.request.nameId, |
|
1181 |
logout.request.sessionIndexes) |
|
1184 | 1182 |
if not found: |
1185 | 1183 |
logger.debug('no third SP session found') |
1186 | 1184 |
else: |
1187 | 1185 |
logger.debug('begin SP sessions processing...') |
1188 | 1186 |
for lib_session in lib_sessions: |
1189 |
p = load_provider(request, lib_session.provider_id, |
|
1190 |
server=logout.server) |
|
1187 |
p = load_provider(request, lib_session.provider_id, server=logout.server) |
|
1191 | 1188 |
if not p: |
1192 | 1189 |
logger.debug('slo cannot logout provider %s, it is no more known.', lib_session.provider_id) |
1193 | 1190 |
continue |
... | ... | |
1200 | 1197 |
logger.debug('%s configured not to receive slo', lib_session.provider_id) |
1201 | 1198 |
if not policy or not policy.forward_slo: |
1202 | 1199 |
lib_sessions.remove(lib_session) |
1203 |
set_session_dump_from_liberty_sessions(logout, |
|
1204 |
found[0:1] + lib_sessions) |
|
1200 |
set_session_dump_from_liberty_sessions(logout, found[0:1] + lib_sessions) |
|
1205 | 1201 |
try: |
1206 | 1202 |
logout.validateRequest() |
1207 | 1203 |
except lasso.LogoutUnsupportedProfileError: |
... | ... | |
1214 | 1210 |
logger.warning('slo, unknown error %s', e) |
1215 | 1211 |
logout.buildResponseMsg() |
1216 | 1212 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1217 |
return return_saml2_response(request, logout, |
|
1218 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1213 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1219 | 1214 |
for lib_session in lib_sessions: |
1220 | 1215 |
try: |
1221 | 1216 |
logger.debug('slo, relaying logout to provider %s', lib_session.provider_id) |
1222 | 1217 |
''' |
1223 | 1218 |
As we are in a synchronous binding, we need SOAP support |
1224 | 1219 |
''' |
1225 |
logout.initRequest(lib_session.provider_id, |
|
1226 |
lasso.HTTP_METHOD_SOAP) |
|
1220 |
logout.initRequest(lib_session.provider_id, lasso.HTTP_METHOD_SOAP) |
|
1227 | 1221 |
logout.buildRequestMsg() |
1228 | 1222 |
if logout.msgBody: |
1229 | 1223 |
logger.debug('slo by SOAP') |
... | ... | |
1246 | 1240 |
logger.debug('kill django sessions') |
1247 | 1241 |
kill_django_sessions(django_session_keys) |
1248 | 1242 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1249 |
return return_saml2_response(request, logout, |
|
1250 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1243 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1251 | 1244 | |
1252 | 1245 | |
1253 | 1246 |
@never_cache |
... | ... | |
1256 | 1249 |
"""Endpoint for receiving SLO by POST, Redirect. |
1257 | 1250 |
""" |
1258 | 1251 |
message = get_saml2_request_message_async_binding(request) |
1259 |
logout, response = process_logout_request(request, message, |
|
1260 |
request.method) |
|
1252 |
logout, response = process_logout_request(request, message, request.method) |
|
1261 | 1253 |
if response: |
1262 | 1254 |
return response |
1263 | 1255 | |
... | ... | |
1266 | 1258 |
LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1267 | 1259 |
except ObjectDoesNotExist: |
1268 | 1260 |
logger.debug('provider %r unknown', logout.remoteProviderId) |
1269 |
return return_logout_error(request, logout, |
|
1270 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1261 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1271 | 1262 |
policy = get_sp_options_policy(provider) |
1272 | 1263 |
if not policy: |
1273 | 1264 |
logger.debug('No policy found for %s', logout.remoteProviderId) |
1274 |
return return_logout_error(request, logout, |
|
1275 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1265 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1276 | 1266 |
if not policy.accept_slo: |
1277 | 1267 |
logger.debug('received slo from %s not authorized', logout.remoteProviderId) |
1278 |
return return_logout_error(request, logout, |
|
1279 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1268 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED) |
|
1280 | 1269 | |
1281 | 1270 |
try: |
1282 | 1271 |
try: |
1283 | 1272 |
logout.processRequestMsg(message) |
1284 |
except (lasso.ServerProviderNotFoundError, |
|
1285 |
lasso.ProfileUnknownProviderError) as e: |
|
1286 |
load_provider(request, logout.remoteProviderId, |
|
1287 |
server=logout.server) |
|
1273 |
except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError): |
|
1274 |
load_provider(request, logout.remoteProviderId, server=logout.server) |
|
1288 | 1275 |
logout.processRequestMsg(message) |
1289 | 1276 |
except lasso.DsError as e: |
1290 | 1277 |
logger.warning('signature error %s', e) |
1291 | 1278 |
logout.buildResponseMsg() |
1292 | 1279 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1293 |
return return_saml2_response(request, logout, |
|
1294 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1295 |
except (lasso.ProfileInvalidMsgError, |
|
1296 |
lasso.ProfileMissingIssuerError) as e: |
|
1280 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1281 |
except (lasso.ProfileInvalidMsgError, lasso.ProfileMissingIssuerError): |
|
1297 | 1282 |
return error_page(request, _('Invalid logout request'), logger=logger, warning=True) |
1298 | 1283 |
session_indexes = logout.request.sessionIndexes |
1299 | 1284 |
if len(session_indexes) == 0: |
1300 |
logger.warning('slo received a request from %s without any SessionIndex, it is forbidden', logout.remoteProviderId) |
|
1285 |
logger.warning('slo received a request from %s without any SessionIndex, it is forbidden', |
|
1286 |
logout.remoteProviderId) |
|
1301 | 1287 |
logout.buildResponseMsg() |
1302 | 1288 |
provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId) |
1303 |
return return_saml2_response(request, logout, |
|
1304 |
title=_('You are being redirected to "%s"') % provider.name) |
|
1289 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
|
1305 | 1290 |
logger.debug('asynchronous slo from %s', logout.remoteProviderId) |
1306 | 1291 |
# Filter sessions |
1307 | 1292 |
if not logout.request.nameId: |
1308 | 1293 |
logger.warning('slo refused, no NameID in the SLO request') |
1309 |
return return_logout_error(request, logout, |
|
1310 |
AUTHENTIC_STATUS_CODE_MISSING_NAMEID) |
|
1294 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_NAMEID) |
|
1311 | 1295 |
all_sessions = LibertySession.get_for_nameid_and_session_indexes( |
1312 |
logout.server.providerId, logout.remoteProviderId, |
|
1313 |
logout.request.nameId, logout.request.sessionIndexes) |
|
1296 |
logout.server.providerId, |
|
1297 |
logout.remoteProviderId, |
|
1298 |
logout.request.nameId, |
|
1299 |
logout.request.sessionIndexes) |
|
1314 | 1300 |
if not all_sessions.exists(): |
1315 | 1301 |
logger.warning('slo refused, since no session exists with the requesting provider') |
1316 |
return return_logout_error(request, logout, |
|
1317 |
AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION) |
|
1302 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION) |
|
1318 | 1303 |
# Load session dump for the requesting provider |
1319 | 1304 |
last_session = all_sessions.latest('creation') |
1320 | 1305 |
set_session_dump_from_liberty_sessions(logout, [last_session]) |
... | ... | |
1322 | 1307 |
logout.validateRequest() |
1323 | 1308 |
except lasso.Error as e: |
1324 | 1309 |
logger.warning('logout request validation failed: %s', e) |
1325 |
return return_logout_error(request, logout, |
|
1326 |
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR) |
|
1310 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR) |
|
1327 | 1311 |
except Exception as e: |
1328 | 1312 |
logger.warning('internal error: %s', e) |
1329 |
return return_logout_error(request, logout, |
|
1330 |
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR) |
|
1313 |
return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR) |
|
1331 | 1314 |
# Now clean sessions for this provider |
1332 |
LibertySession.objects.filter(provider_id=logout.remoteProviderId, |
|
1333 |
django_session_key=request.session.session_key).delete() |
|
1315 |
LibertySession.objects.filter( |
|
1316 |
provider_id=logout.remoteProviderId, |
|
1317 |
django_session_key=request.session.session_key).delete() |
|
1334 | 1318 |
# Save some values for cleaning up |
1335 |
save_key_values(logout.request.id, logout.dump(), |
|
1336 |
request.session.session_key) |
|
1319 |
save_key_values(logout.request.id, logout.dump(), request.session.session_key) |
|
1337 | 1320 | |
1338 | 1321 |
# Use the logout view and come back to the finish slo view |
1339 | 1322 |
next_url = make_url(finish_slo, params={'id': logout.request.id}) |
... | ... | |
1343 | 1326 |
def icon_url(name): |
1344 | 1327 |
return '%s/authentic2/images/%s.png' % (settings.STATIC_URL, name) |
1345 | 1328 | |
1329 | ||
1346 | 1330 |
def ko_icon(request): |
1347 | 1331 |
return HttpResponseRedirect(icon_url('ko'), status=307) |
1348 | 1332 | |
1333 | ||
1349 | 1334 |
def ok_icon(request): |
1350 | 1335 |
return HttpResponseRedirect(icon_url('ok'), status=307) |
1351 | 1336 | |
... | ... | |
1404 | 1389 |
logout.request.sessionIndexes = [] |
1405 | 1390 |
else: |
1406 | 1391 |
session_indexes = lib_sessions.values_list('session_index', flat=True) |
1407 |
logout.request.sessionIndexes = tuple(map(lambda x: x.encode('utf8'), |
|
1408 |
session_indexes)) |
|
1392 |
logout.request.sessionIndexes = tuple(map(lambda x: x.encode('utf8'), session_indexes)) |
|
1409 | 1393 |
logout.msgRelayState = logout.request.id |
1410 | 1394 |
try: |
1411 | 1395 |
logout.buildRequestMsg() |
... | ... | |
1435 | 1419 |
logger.warning('slo error: %s', e) |
1436 | 1420 |
else: |
1437 | 1421 |
LibertySession.objects.filter( |
1438 |
django_session_key=request.session.session_key,
|
|
1439 |
provider_id=logout.remoteProviderId).delete()
|
|
1422 |
django_session_key=request.session.session_key, |
|
1423 |
provider_id=logout.remoteProviderId).delete() |
|
1440 | 1424 |
logger.debug('deleted session to %s', logout.remoteProviderId) |
1441 | 1425 |
return redirect_next(request, next) or ok_icon(request) |
1442 | 1426 | |
... | ... | |
1445 | 1429 |
def slo_return(request): |
1446 | 1430 |
relay_state = request.GET.get('RelayState') |
1447 | 1431 |
if not relay_state: |
1448 |
return error_redirect(request, N_('slo no relay state in response'), |
|
1449 |
default_url=icon_url('ko')) |
|
1432 |
return error_redirect(request, N_('slo no relay state in response'), default_url=icon_url('ko')) |
|
1450 | 1433 |
logger.debug('relay_state %r', relay_state) |
1451 | 1434 |
try: |
1452 | 1435 |
logout_dump, provider_id, next = \ |
1453 | 1436 |
get_and_delete_key_values(relay_state) |
1454 | 1437 |
except KeyError: |
1455 |
return error_redirect(request, |
|
1456 |
N_('unknown relay state %r'), |
|
1457 |
relay_state, |
|
1458 |
default_url=icon_url('ko')) |
|
1438 |
return error_redirect(request, N_('unknown relay state %r'), relay_state, default_url=icon_url('ko')) |
|
1459 | 1439 |
server = create_server(request) |
1460 | 1440 |
logout = lasso.Logout.newFromDump(server, logout_dump) |
1461 | 1441 |
provider_id = logout.remoteProviderId |
... | ... | |
1467 | 1447 |
logout.setSignatureVerifyHint(lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE) |
1468 | 1448 |
if not load_provider(request, provider_id, server=logout.server): |
1469 | 1449 |
logger.warning('failed to load provider %s', provider_id) |
1470 |
return process_logout_response(request, logout, |
|
1471 |
get_saml2_query_request(request), next) |
|
1450 |
return process_logout_response(request, logout, get_saml2_query_request(request), next) |
|
1472 | 1451 | |
1473 | 1452 |
# Helpers |
1474 | 1453 | |
1475 | 1454 |
# Mapping to generate the metadata file, must be kept in sync with the url |
1476 | 1455 |
# dispatcher |
1477 | 1456 | |
1457 | ||
1478 | 1458 |
def get_provider_id_and_options(request, provider_id): |
1479 | 1459 |
if not provider_id: |
1480 | 1460 |
provider_id = reverse(metadata) |
1481 | 1461 |
options = { |
1482 |
'key': app_settings.SIGNATURE_PUBLIC_KEY,
|
|
1483 |
'private_key': app_settings.SIGNATURE_PRIVATE_KEY,
|
|
1462 |
'key': app_settings.SIGNATURE_PUBLIC_KEY, |
|
1463 |
'private_key': app_settings.SIGNATURE_PRIVATE_KEY, |
|
1484 | 1464 |
} |
1485 | 1465 |
options.update(app_settings.METADATA_OPTIONS) |
1486 | 1466 |
return provider_id, options |
... | ... | |
1493 | 1473 |
settings.py. |
1494 | 1474 |
''' |
1495 | 1475 |
provider_id, options = get_provider_id_and_options(request, provider_id) |
1496 |
return get_saml2_metadata(request, request.path, idp_map=metadata_map, |
|
1497 |
options=options) |
|
1476 |
return get_saml2_metadata(request, request.path, idp_map=metadata_map, options=options) |
|
1498 | 1477 | |
1499 | 1478 | |
1500 | 1479 |
def create_server(request, provider_id=None): |
... | ... | |
1504 | 1483 |
multithreading is used, then thread local storage should be used. |
1505 | 1484 |
''' |
1506 | 1485 |
provider_id, options = get_provider_id_and_options(request, provider_id) |
1507 |
__cached_server = create_saml2_server(request, provider_id, |
|
1508 |
idp_map=metadata_map, options=options) |
|
1486 |
__cached_server = create_saml2_server(request, provider_id, idp_map=metadata_map, options=options) |
|
1509 | 1487 |
return __cached_server |
1510 | 1488 | |
1511 | 1489 | |
1512 | 1490 |
def log_info_authn_request_details(login): |
1513 | 1491 |
'''Push to logs details abour the received AuthnRequest''' |
1514 | 1492 |
request = login.request |
1515 |
details = {'issuer': login.request.issuer and login.request.issuer.content, |
|
1516 |
'forceAuthn': login.request.forceAuthn, |
|
1517 |
'isPassive': login.request.isPassive, |
|
1518 |
'protocolBinding': login.request.protocolBinding} |
|
1493 |
details = { |
|
1494 |
'issuer': login.request.issuer and login.request.issuer.content, |
|
1495 |
'forceAuthn': login.request.forceAuthn, |
|
1496 |
'isPassive': login.request.isPassive, |
|
1497 |
'protocolBinding': login.request.protocolBinding, |
|
1498 |
} |
|
1519 | 1499 |
nameIdPolicy = request.nameIdPolicy |
1520 | 1500 |
if nameIdPolicy: |
1521 | 1501 |
details['nameIdPolicy'] = { |
1522 |
'allowCreate': nameIdPolicy.allowCreate, |
|
1523 |
'format': nameIdPolicy.format, |
|
1524 |
'spNameQualifier': nameIdPolicy.spNameQualifier} |
|
1502 |
'allowCreate': nameIdPolicy.allowCreate, |
|
1503 |
'format': nameIdPolicy.format, |
|
1504 |
'spNameQualifier': nameIdPolicy.spNameQualifier, |
|
1505 |
} |
|
1525 | 1506 |
logger.debug('%r' % details) |
1526 | 1507 | |
1527 | 1508 | |
... | ... | |
1533 | 1514 |
logger.warning('failure, expected: %r got: %r ', destination, req_or_res.destination) |
1534 | 1515 |
return result |
1535 | 1516 | |
1517 | ||
1536 | 1518 |
def error_redirect(request, msg, *args, **kwargs): |
1537 | 1519 |
'''Log a warning message, register it with the messages framework, then |
1538 | 1520 |
redirect the user to the homepage. |
... | ... | |
1540 | 1522 |
It will redirect to Authentic2 homepage unless a next query parameter was used. |
1541 | 1523 |
''' |
1542 | 1524 |
default_kwargs = { |
1543 |
'log_level': logging.WARNING,
|
|
1544 |
'msg_level': messages.WARNING,
|
|
1545 |
'default_url': None,
|
|
1525 |
'log_level': logging.WARNING, |
|
1526 |
'msg_level': messages.WARNING, |
|
1527 |
'default_url': None, |
|
1546 | 1528 |
} |
1547 | 1529 |
default_kwargs.update(kwargs) |
1548 | 1530 |
messages.add_message(request, default_kwargs['msg_level'], _(msg) % args) |
src/authentic2/idp/saml/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 | 18 | |
3 | 19 |
from . import views |
src/authentic2/idp/saml/views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import ugettext as _ |
2 | 18 |
from django.core.urlresolvers import reverse |
3 | 19 |
from django.views.generic import DeleteView, View |
... | ... | |
8 | 24 | |
9 | 25 |
from authentic2.saml.models import LibertyFederation |
10 | 26 | |
27 | ||
11 | 28 |
class FederationCreateView(View): |
12 | 29 |
pass |
13 | 30 | |
31 | ||
14 | 32 |
class FederationDeleteView(DeleteView): |
15 | 33 |
model = LibertyFederation |
16 | 34 | |
... | ... | |
28 | 46 |
return HttpResponseRedirect(self.get_success_url()) |
29 | 47 | |
30 | 48 |
def get_success_url(self): |
31 |
return self.request.POST.get(REDIRECT_FIELD_NAME, |
|
32 |
reverse('auth_homepage')) |
|
49 |
return self.request.POST.get(REDIRECT_FIELD_NAME, reverse('auth_homepage')) |
|
33 | 50 | |
34 | 51 | |
35 | 52 |
delete_federation = FederationDeleteView.as_view() |
src/authentic2/idp/signals.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.dispatch import Signal |
2 | 18 | |
3 | 19 |
'''authorize_decision |
... | ... | |
5 | 21 |
- the authorization decision e.g. dic['authz'] = True or False |
6 | 22 |
- optionnaly a message e.g. dic['message'] = message |
7 | 23 |
''' |
8 |
authorize_service = Signal(providing_args = ["request", "user", "audience", |
|
9 |
"attributes"]) |
|
24 |
authorize_service = Signal(providing_args=["request", "user", "audience", "attributes"]) |
|
10 | 25 | |
11 | 26 |
'''avoid_consent |
12 | 27 |
Expect a boolean e.g. dic['avoid_consent'] = True or False |
13 | 28 |
''' |
14 |
avoid_consent = Signal(providing_args = ["request", "user", "audience"]) |
|
29 |
avoid_consent = Signal(providing_args=["request", "user", "audience"]) |
src/authentic2/idp/templatetags/breadcrumbs.py | ||
---|---|---|
1 |
# This is a copy of http://djangosnippets.org/snippets/1289/ |
|
2 |
# |
|
3 |
# it provides two template tags to use in HTML templates: breadcrumb and |
|
4 |
# breadcrumb_url. |
|
5 |
# |
|
6 |
# The first allows creating of simple url, with the text portion and url |
|
7 |
# portion. Or only unlinked text (as the last item in breadcrumb trail for |
|
8 |
# example). The second, can actually take the named url with arguments. |
|
9 |
# Additionally it takes a title as the first argument. |
|
10 |
# |
|
11 |
# Initial Author: Andriy Drozdyuk |
|
12 | ||
13 | ||
14 |
from django import template |
|
15 |
from django.template import loader, Node, Variable |
|
16 |
from django.utils.encoding import smart_str, smart_unicode |
|
17 |
from django.template.defaulttags import url |
|
18 |
from django.template import VariableDoesNotExist |
|
19 | ||
20 |
register = template.Library() |
|
21 | ||
22 |
@register.tag |
|
23 |
def breadcrumb(parser, token): |
|
24 |
""" |
|
25 |
Renders the breadcrumb. |
|
26 |
Examples: |
|
27 |
{% breadcrumb "Title of breadcrumb" url_var %} |
|
28 |
{% breadcrumb context_var url_var %} |
|
29 |
{% breadcrumb "Just the title" %} |
|
30 |
{% breadcrumb just_context_var %} |
|
31 | ||
32 |
Parameters: |
|
33 |
-First parameter is the title of the crumb, |
|
34 |
-Second (optional) parameter is the url variable to link to, produced by url tag, i.e.: |
|
35 |
{% url 'person_detail' object.id as person_url %} |
|
36 |
then: |
|
37 |
{% breadcrumb person.name person_url %} |
|
38 | ||
39 |
@author Andriy Drozdyuk |
|
40 |
""" |
|
41 |
return BreadcrumbNode(token.split_contents()[1:]) |
|
42 | ||
43 | ||
44 |
@register.tag |
|
45 |
def breadcrumb_url(parser, token): |
|
46 |
""" |
|
47 |
Same as breadcrumb |
|
48 |
but instead of url context variable takes in all the |
|
49 |
arguments URL tag takes. |
|
50 |
{% breadcrumb "Title of breadcrumb" person_detail person.id %} |
|
51 |
{% breadcrumb person.name person_detail person.id %} |
|
52 |
""" |
|
53 | ||
54 |
bits = token.split_contents() |
|
55 |
if len(bits)==2: |
|
56 |
return breadcrumb(parser, token) |
|
57 | ||
58 |
# Extract our extra title parameter |
|
59 |
title = bits.pop(1) |
|
60 |
token.contents = ' '.join(bits) |
|
61 | ||
62 |
url_node = url(parser, token) |
|
63 | ||
64 |
return UrlBreadcrumbNode(title, url_node) |
|
65 | ||
66 | ||
67 |
class BreadcrumbNode(Node): |
|
68 |
def __init__(self, vars): |
|
69 |
""" |
|
70 |
First var is title, second var is url context variable |
|
71 |
""" |
|
72 |
self.vars = map(Variable,vars) |
|
73 | ||
74 |
def render(self, context): |
|
75 |
title = self.vars[0].var |
|
76 | ||
77 |
if title.find("'")==-1 and title.find('"')==-1: |
|
78 |
try: |
|
79 |
val = self.vars[0] |
|
80 |
title = val.resolve(context) |
|
81 |
except: |
|
82 |
title = '' |
|
83 | ||
84 |
else: |
|
85 |
title=title.strip("'").strip('"') |
|
86 |
title=smart_unicode(title) |
|
87 | ||
88 |
url = None |
|
89 | ||
90 |
if len(self.vars)>1: |
|
91 |
val = self.vars[1] |
|
92 |
try: |
|
93 |
url = val.resolve(context) |
|
94 |
except VariableDoesNotExist: |
|
95 |
url = None |
|
96 | ||
97 |
return create_crumb(title, url) |
|
98 | ||
99 | ||
100 |
class UrlBreadcrumbNode(Node): |
|
101 |
def __init__(self, title, url_node): |
|
102 |
self.title = Variable(title) |
|
103 |
self.url_node = url_node |
|
104 | ||
105 |
def render(self, context): |
|
106 |
title = self.title.var |
|
107 | ||
108 |
if title.find("'")==-1 and title.find('"')==-1: |
|
109 |
try: |
|
110 |
val = self.title |
|
111 |
title = val.resolve(context) |
|
112 |
except: |
|
113 |
title = '' |
|
114 |
else: |
|
115 |
title=title.strip("'").strip('"') |
|
116 |
title=smart_unicode(title) |
|
117 | ||
118 |
url = self.url_node.render(context) |
|
119 |
return create_crumb(title, url) |
|
120 | ||
121 | ||
122 |
def create_crumb(title, url=None): |
|
123 |
""" |
|
124 |
Helper function |
|
125 |
""" |
|
126 |
crumb = """<span class="breadcrumbs-arrow">""" \ |
|
127 |
""" > """ \ |
|
128 |
"""</span>""" |
|
129 |
if url: |
|
130 |
crumb = "%s <a href='%s'>%s</a>" % (crumb, url, title) |
|
131 |
else: |
|
132 |
crumb = "%s %s" % (crumb, title) |
|
133 | ||
134 |
return crumb |
src/authentic2/idp/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 |
from authentic2.idp.interactions import consent_federation, consent_attributes
|
|
18 |
from authentic2.idp.interactions import consent_federation |
|
3 | 19 | |
4 | 20 |
urlpatterns = [ |
5 | 21 |
url(r'^consent_federation', consent_federation, |
6 | 22 |
name='a2-consent-federation'), |
7 |
url(r'^consent_attributes', consent_attributes, |
|
8 |
name='a2-consent-attributes') |
|
9 | 23 |
] |
src/authentic2/ldap_utils.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
2 | 17 | |
3 | 18 |
import string |
4 | 19 |
src/authentic2/log_filters.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
from django.utils import six |
3 | 19 | |
20 | ||
4 | 21 |
class RequestContextFilter(logging.Filter): |
5 | 22 |
DEFAULT_USERNAME = '-' |
6 | 23 |
DEFAULT_IP = '-' |
... | ... | |
16 | 33 |
user = self.DEFAULT_USERNAME |
17 | 34 |
ip = self.DEFAULT_IP |
18 | 35 |
request_id = self.DEFAULT_REQUEST_ID |
19 |
if not request is None:
|
|
36 |
if request is not None:
|
|
20 | 37 |
if hasattr(request, 'user') and request.user.is_authenticated(): |
21 | 38 |
user = six.text_type(request.user) |
22 | 39 |
ip = request.META.get('REMOTE_ADDR', self.DEFAULT_IP) |
src/authentic2/logger.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
19 | ||
3 | 20 |
class SettingsLogLevel(int): |
4 | 21 |
def __new__(cls, default_log_level, debug_setting='DEBUG'): |
5 | 22 |
return super(SettingsLogLevel, cls).__new__( |
... | ... | |
9 | 26 |
self.debug_setting = debug_setting |
10 | 27 |
super(SettingsLogLevel, self).__init__() |
11 | 28 | |
29 | ||
12 | 30 |
class DjangoLogger(logging.getLoggerClass()): |
13 | 31 |
def getEffectiveLevel(self): |
14 | 32 |
level = super(DjangoLogger, self).getEffectiveLevel() |
... | ... | |
21 | 39 | |
22 | 40 |
logging.setLoggerClass(DjangoLogger) |
23 | 41 | |
42 | ||
24 | 43 |
class DjangoRootLogger(DjangoLogger, logging.RootLogger): |
25 | 44 |
pass |
26 | 45 |
src/authentic2/management/commands/clean-unused-accounts.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import logging |
... | ... | |
13 | 29 | |
14 | 30 |
from django.conf import settings |
15 | 31 | |
32 |
logger = logging.getLogger(__name__) |
|
33 | ||
34 | ||
16 | 35 |
def print_table(table): |
17 | 36 |
col_width = [max(len(x) for x in col) for col in zip(*table)] |
18 | 37 |
for line in table: |
19 |
line = u"| " + u" | ".join(u"{0:>{1}}".format(x, col_width[i]) |
|
20 |
for i, x in enumerate(line)) + u" |" |
|
38 |
line = u"| " + u" | ".join(u"{0:>{1}}".format(x, col_width[i]) for i, x in enumerate(line)) + u" |" |
|
21 | 39 |
print(line) |
22 | 40 | |
41 | ||
23 | 42 |
class Command(BaseCommand): |
24 | 43 |
help = '''Clean unused accounts''' |
25 | 44 | |
... | ... | |
48 | 67 |
) |
49 | 68 | |
50 | 69 |
def handle(self, *args, **options): |
51 |
log = logging.getLogger(__name__) |
|
52 | 70 |
try: |
53 | 71 |
self.clean_unused_acccounts(*args, **options) |
54 |
except: |
|
55 |
log.exception('failure while cleaning unused accounts') |
|
72 |
except Exception:
|
|
73 |
logger.exception('failure while cleaning unused accounts')
|
|
56 | 74 | |
57 | 75 |
def clean_unused_acccounts(self, *args, **options): |
58 | 76 |
if options['period'] < 1: |
... | ... | |
71 | 89 |
elif options['verbosity'] == '3': |
72 | 90 |
logging.basicConfig(level=logging.DEBUG) |
73 | 91 | |
74 |
log = logging.getLogger(__name__) |
|
75 | 92 |
n = now().replace(hour=0, minute=0, second=0, microsecond=0) |
76 | 93 |
self.fake = options['fake'] |
77 | 94 |
self.from_email = options['from_email'] |
78 | 95 |
if self.fake: |
79 |
log.info('fake call to clean-unused-accounts') |
|
96 |
logger.info('fake call to clean-unused-accounts')
|
|
80 | 97 |
users = get_user_model().objects.all() |
81 | 98 |
if options['filter']: |
82 | 99 |
for f in options['filter']: |
83 | 100 |
key, value = f.split('=', 1) |
84 | 101 |
try: |
85 | 102 |
users = users.filter(**{key: value}) |
86 |
except: |
|
103 |
except Exception:
|
|
87 | 104 |
raise CommandError('invalid --filter %s' % f) |
88 | 105 |
if options['alert_thresholds']: |
89 | 106 |
alert_thresholds = options['alert_thresholds'] |
... | ... | |
91 | 108 |
try: |
92 | 109 |
alert_thresholds = map(int, alert_thresholds) |
93 | 110 |
except ValueError: |
94 |
raise CommandError('alert_thresholds must be a comma ' |
|
95 |
'separated list of integers') |
|
111 |
raise CommandError('alert_thresholds must be a comma separated list of integers') |
|
96 | 112 |
for threshold in alert_thresholds: |
97 | 113 |
if not (0 < threshold < clean_threshold): |
98 |
raise CommandError('alert-threshold must a positive integer '
|
|
99 |
'inferior to clean-threshold: 0 < %d < %d' % (
|
|
100 |
threshold, clean_threshold))
|
|
114 |
raise CommandError( |
|
115 |
'alert-threshold must a positive integer inferior to clean-threshold: 0 < %d < %d' % (
|
|
116 |
threshold, clean_threshold)) |
|
101 | 117 |
for threshold in alert_thresholds: |
102 | 118 |
a = n - datetime.timedelta(days=threshold) |
103 |
b = n - datetime.timedelta(days=threshold-options['period'])
|
|
119 |
b = n - datetime.timedelta(days=threshold - options['period'])
|
|
104 | 120 |
for user in users.filter(last_login__lt=b, last_login__gte=a): |
105 |
log.info('%s last login %d days ago, sending alert', user, threshold) |
|
106 |
self.send_alert(user, threshold, clean_threshold-threshold)
|
|
121 |
logger.info('%s last login %d days ago, sending alert', user, threshold)
|
|
122 |
self.send_alert(user, threshold, clean_threshold - threshold)
|
|
107 | 123 |
threshold = n - datetime.timedelta(days=clean_threshold) |
108 | 124 |
for user in users.filter(last_login__lt=threshold): |
109 | 125 |
d = n - user.last_login |
110 |
log.info('%s last login %d days ago, deleting user', user, d.days) |
|
126 |
logger.info('%s last login %d days ago, deleting user', user, d.days)
|
|
111 | 127 |
self.delete_user(user, clean_threshold) |
112 | 128 | |
113 | ||
114 | 129 |
def send_alert(self, user, threshold, clean_threshold): |
115 |
ctx = { 'user': user, 'threshold': threshold, |
|
116 |
'clean_threshold': clean_threshold } |
|
130 |
ctx = { |
|
131 |
'user': user, |
|
132 |
'threshold': threshold, |
|
133 |
'clean_threshold': clean_threshold |
|
134 |
} |
|
117 | 135 |
self.send_mail('authentic2/unused_account_alert', user, ctx) |
118 | 136 | |
119 | ||
120 | 137 |
def send_mail(self, prefix, user, ctx): |
121 |
log = logging.getLogger(__name__) |
|
122 | ||
123 | 138 |
if not user.email: |
124 |
log.debug('%s has no email, no mail sent', user) |
|
139 |
logger.debug('%s has no email, no mail sent', user)
|
|
125 | 140 |
subject = render_to_string(prefix + '_subject.txt', ctx).strip() |
126 | 141 |
body = render_to_string(prefix + '_body.txt', ctx) |
127 | 142 |
if not self.fake: |
128 | 143 |
try: |
129 |
log.debug('sending mail to %s', user.email) |
|
144 |
logger.debug('sending mail to %s', user.email)
|
|
130 | 145 |
send_mail(subject, body, self.from_email, [user.email]) |
131 |
except: |
|
132 |
log.exception('email sending failure') |
|
133 | ||
146 |
except Exception: |
|
147 |
logger.exception('email sending failure') |
|
134 | 148 | |
135 | 149 |
def delete_user(self, user, threshold): |
136 |
ctx = { 'user': user, 'threshold': threshold } |
|
137 |
self.send_mail('authentic2/unused_account_delete', user, |
|
138 |
ctx) |
|
150 |
ctx = { |
|
151 |
'user': user, |
|
152 |
'threshold': threshold |
|
153 |
} |
|
154 |
self.send_mail('authentic2/unused_account_delete', user, ctx) |
|
139 | 155 |
if not self.fake: |
140 | 156 |
DeletedUser.objects.delete_user(user) |
src/authentic2/management/commands/export_site.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import json |
2 | 18 |
import sys |
3 | 19 | |
4 | 20 |
from django.core.management.base import BaseCommand |
5 | 21 | |
6 | 22 |
from authentic2.data_transfer import export_site |
7 |
from django_rbac.utils import get_role_model |
|
8 | 23 | |
9 | 24 | |
10 | 25 |
class Command(BaseCommand): |
src/authentic2/management/commands/import_site.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import contextlib |
2 | 18 |
import json |
3 | 19 |
import sys |
src/authentic2/management/commands/load-ldif.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import argparse |
2 | 18 |
import logging |
3 | 19 |
import json |
... | ... | |
5 | 21 | |
6 | 22 |
from django.core.management.base import BaseCommand |
7 | 23 |
from django.contrib.auth import get_user_model |
24 |
from django.db.transaction import atomic |
|
8 | 25 | |
9 | 26 | |
10 |
from authentic2.compat import atomic |
|
11 | 27 |
from authentic2.hashers import olap_password_to_dj |
12 | 28 |
from authentic2.models import Attribute |
13 | 29 | |
... | ... | |
31 | 47 |
self.callback = d.get('callback') |
32 | 48 |
ldif.LDIFParser.__init__(self, *args, **kwargs) |
33 | 49 | |
34 | ||
35 | 50 |
def handle(self, dn, entry): |
36 | 51 |
User = get_user_model() |
37 | 52 |
if self.object_class not in entry['objectClass']: |
... | ... | |
63 | 78 |
m.extend(self.callback(u, dn, entry, self.options, d)) |
64 | 79 |
if 'username' not in d: |
65 | 80 |
self.log.warning('cannot load dn %s, username cannot be initialized from the field %s', |
66 |
dn, self.options['username']) |
|
81 |
dn, self.options['username'])
|
|
67 | 82 |
return |
68 | 83 |
try: |
69 | 84 |
old = User.objects.get(username=d['username']) |
... | ... | |
77 | 92 |
def parse(self, *args, **kwargs): |
78 | 93 |
ldif.LDIFParser.parse(self, *args, **kwargs) |
79 | 94 |
if self.options['result']: |
80 |
with file(self.options['result'], 'w') as f:
|
|
95 |
with open(self.options['result'], 'w') as f:
|
|
81 | 96 |
json.dump(self.json, f) |
82 | 97 | |
83 | 98 | |
... | ... | |
86 | 101 |
def __call__(self, parser, namespace, values, option_string=None): |
87 | 102 |
ldap_attribute, django_attribute = values |
88 | 103 |
try: |
89 |
attribute = Attribute.objects.get(name=django_attribute)
|
|
104 |
Attribute.objects.get(name=django_attribute) |
|
90 | 105 |
except Attribute.DoesNotExist: |
91 |
raise argparse.ArgumentTypeError( |
|
92 |
'django attribute %s does not exist' % django_attribute) |
|
106 |
raise argparse.ArgumentTypeError('django attribute %s does not exist' % django_attribute) |
|
93 | 107 |
res = getattr(namespace, self.dest, {}) |
94 | 108 |
res[ldap_attribute] = django_attribute |
95 | 109 |
setattr(namespace, self.dest, res) |
... | ... | |
138 | 152 |
options['verbosity'] = int(options['verbosity']) |
139 | 153 |
ldif_files = options.pop('ldif_file') |
140 | 154 |
for arg in ldif_files: |
141 |
f = file(arg)
|
|
155 |
f = open(arg)
|
|
142 | 156 |
parser = DjangoUserLDIFParser(f, options=options, command=self) |
143 | 157 |
parser.parse() |
144 | 158 |
if not options['fake']: |
src/authentic2/management/commands/resetpassword.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import getpass |
2 | 18 | |
3 | 19 |
from django.contrib.auth import get_user_model |
... | ... | |
7 | 23 |
from authentic2.utils import generate_password |
8 | 24 |
from authentic2.models import PasswordReset |
9 | 25 | |
26 | ||
27 |
User = get_user_model() |
|
28 | ||
29 | ||
10 | 30 |
class Command(BaseCommand): |
11 | 31 |
help = "Reset a user's password for django.contrib.auth." |
12 | 32 | |
... | ... | |
29 | 49 |
if not username: |
30 | 50 |
username = getpass.getuser() |
31 | 51 | |
32 |
UserModel = get_user_model() |
|
33 | ||
34 | 52 |
try: |
35 |
u = UserModel._default_manager.using(options.get('database')).get(**{
|
|
36 |
UserModel.USERNAME_FIELD: username
|
|
37 |
})
|
|
38 |
except UserModel.DoesNotExist:
|
|
53 |
u = User._default_manager.using(options.get('database')).get(**{ |
|
54 |
User.USERNAME_FIELD: username
|
|
55 |
}) |
|
56 |
except User.DoesNotExist: |
|
39 | 57 |
raise CommandError("user '%s' does not exist" % username) |
40 | 58 | |
41 | 59 |
p1 = generate_password() |
... | ... | |
43 | 61 |
u.set_password(p1) |
44 | 62 |
u.save() |
45 | 63 |
PasswordReset.objects.get_or_create(user=u) |
46 |
return "Password changed successfully for user '%s', on next login he will be forced to change its password." % u |
|
64 |
return ( |
|
65 |
'Password changed successfully for user "%s", on next login he ' |
|
66 |
'will be forced to change its password.' % u) |
|
47 | 67 |
src/authentic2/management/commands/slapd-shell.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import logging |
... | ... | |
11 | 27 |
from django.contrib.auth import get_user_model |
12 | 28 |
from django.core.management.base import BaseCommand |
13 | 29 |
from django.utils import six |
14 |
from optparse import make_option |
|
15 | 30 | |
16 | 31 |
COMMAND = 1 |
17 | 32 |
ATTR = 2 |
... | ... | |
24 | 39 |
'email': 'mail', |
25 | 40 |
} |
26 | 41 | |
42 | ||
27 | 43 |
def unescape_filter_chars(s): |
28 | 44 |
return re.sub(r'\\..', lambda s: s.group()[1:].decode('hex'), s) |
29 | 45 | |
46 | ||
30 | 47 |
class Command(BaseCommand): |
31 | 48 |
help = 'OpenLDAP shell backend' |
32 | 49 | |
33 | ||
34 | 50 |
def ldap(self, command, attrs): |
35 | 51 |
self.logger.debug('received command %s %s', command, attrs) |
36 | 52 |
if command == 'SEARCH': |
src/authentic2/management/commands/sync-ldap-users.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
try: |
2 | 18 |
import ldap |
3 |
from ldap.filter import filter_format |
|
19 |
from ldap.filter import filter_format # noqa: F401
|
|
4 | 20 |
except ImportError: |
5 | 21 |
ldap = None |
6 | 22 | |
7 |
from django.core.management.base import BaseCommand, CommandError
|
|
23 |
from django.core.management.base import BaseCommand |
|
8 | 24 | |
9 | 25 |
from authentic2.backends.ldap_backend import LDAPBackend |
10 | 26 | |
11 |
class Command(BaseCommand): |
|
12 | 27 | |
28 |
class Command(BaseCommand): |
|
13 | 29 |
def handle(self, *args, **kwargs): |
14 | 30 |
list(LDAPBackend.get_users()) |
src/authentic2/manager/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
default_app_config = 'authentic2.manager.apps.AppConfig' |
src/authentic2/manager/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import sys |
2 | 18 | |
3 | 19 |
src/authentic2/manager/apps.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.apps import AppConfig |
2 | 18 | |
3 | 19 | |
... | ... | |
8 | 24 |
def ready(self): |
9 | 25 |
from django.db.models.signals import post_save |
10 | 26 |
from django_rbac.utils import get_ou_model |
11 |
from django_select2 import conf |
|
12 | 27 | |
13 | 28 |
post_save.connect( |
14 | 29 |
self.post_save_ou, |
src/authentic2/manager/fields.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django import forms |
2 | 18 | |
3 | 19 |
from . import widgets |
src/authentic2/manager/forms.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import hashlib |
2 | 18 |
import smtplib |
3 | 19 |
import logging |
... | ... | |
5 | 21 |
from django.utils.translation import ugettext_lazy as _, pgettext |
6 | 22 |
from django import forms |
7 | 23 |
from django.contrib.contenttypes.models import ContentType |
24 |
from django.contrib.auth import get_user_model |
|
8 | 25 |
from django.db.models.query import Q |
9 | 26 |
from django.utils import six |
10 | 27 |
from django.utils.text import slugify |
11 | 28 |
from django.core.exceptions import ValidationError |
12 | 29 | |
13 |
from authentic2.compat import get_user_model |
|
14 | 30 |
from authentic2.passwords import generate_password |
15 | 31 |
from authentic2.utils import send_templated_mail |
16 | 32 |
from authentic2.forms.fields import NewPasswordField, CheckPasswordField |
... | ... | |
19 | 35 |
from django_rbac.utils import get_ou_model, get_role_model, get_permission_model |
20 | 36 |
from django_rbac.backends import DjangoRBACBackend |
21 | 37 | |
22 |
from authentic2.forms import BaseUserForm |
|
38 |
from authentic2.forms.profile import BaseUserForm
|
|
23 | 39 |
from authentic2.models import PasswordReset |
24 | 40 |
from authentic2.utils import import_module_or_class |
25 | 41 |
from authentic2.a2_rbac.utils import get_default_ou |
... | ... | |
28 | 44 | |
29 | 45 |
from . import fields, app_settings, utils |
30 | 46 | |
47 |
User = get_user_model() |
|
31 | 48 | |
32 | 49 |
logger = logging.getLogger(__name__) |
33 | 50 | |
... | ... | |
194 | 211 |
self.data._mutable = False |
195 | 212 | |
196 | 213 |
def clean(self): |
197 |
if (self.instance.has_usable_password() and (
|
|
198 |
'username' in self.fields or
|
|
199 |
'email' in self.fields)): |
|
214 |
if (self.instance.has_usable_password() |
|
215 |
and ('username' in self.fields
|
|
216 |
or 'email' in self.fields)):
|
|
200 | 217 |
if not self.cleaned_data.get('username') and \ |
201 | 218 |
not self.cleaned_data.get('email'): |
202 | 219 |
raise forms.ValidationError( |
203 | 220 |
_('You must set a username or an email.')) |
204 | 221 | |
205 |
User = get_user_model() |
|
206 | 222 |
if self.cleaned_data.get('email'): |
207 | 223 |
qs = User.objects.all() |
208 | 224 |
ou = getattr(self, 'ou', None) |
... | ... | |
226 | 242 |
}) |
227 | 243 | |
228 | 244 |
class Meta: |
229 |
model = get_user_model()
|
|
245 |
model = User
|
|
230 | 246 |
exclude = ('is_staff', 'groups', 'user_permissions', 'last_login', |
231 | 247 |
'date_joined', 'password') |
232 | 248 | |
... | ... | |
251 | 267 | |
252 | 268 |
def clean(self): |
253 | 269 |
super(UserChangePasswordForm, self).clean() |
254 |
if (self.require_password and
|
|
255 |
not self.cleaned_data.get('generate_password') and
|
|
256 |
not self.cleaned_data.get('password1') and
|
|
257 |
not self.cleaned_data.get('send_password_reset')): |
|
270 |
if (self.require_password |
|
271 |
and not self.cleaned_data.get('generate_password')
|
|
272 |
and not self.cleaned_data.get('password1')
|
|
273 |
and not self.cleaned_data.get('send_password_reset')):
|
|
258 | 274 |
raise forms.ValidationError( |
259 | 275 |
_('You must choose password generation or type a new' |
260 | 276 |
' one or send a password reset mail')) |
261 |
if (not self.has_email() and
|
|
262 |
(self.cleaned_data.get('send_mail') or
|
|
263 |
self.cleaned_data.get('generate_password' or
|
|
264 |
self.cleaned_data.get('send_password_reset')))):
|
|
277 |
if (not self.has_email() |
|
278 |
and (self.cleaned_data.get('send_mail')
|
|
279 |
or self.cleaned_data.get('generate_password')
|
|
280 |
or self.cleaned_data.get('send_password_reset'))):
|
|
265 | 281 |
raise forms.ValidationError( |
266 | 282 |
_('User does not have a mail, we cannot send the ' |
267 | 283 |
'informations to him.')) |
... | ... | |
310 | 326 |
required=False) |
311 | 327 | |
312 | 328 |
class Meta: |
313 |
model = get_user_model()
|
|
329 |
model = User
|
|
314 | 330 |
fields = () |
315 | 331 | |
316 | 332 | |
... | ... | |
340 | 356 |
# check if this account is going to be real online account, i.e. with a |
341 | 357 |
# password, it it's the case complain that there is no identifiers. |
342 | 358 |
has_password = ( |
343 |
self.cleaned_data.get('new_password1') or
|
|
344 |
self.cleaned_data.get('generate_password') or
|
|
345 |
self.cleaned_data.get('send_password_reset')) |
|
359 |
self.cleaned_data.get('new_password1') |
|
360 |
or self.cleaned_data.get('generate_password')
|
|
361 |
or self.cleaned_data.get('send_password_reset'))
|
|
346 | 362 | |
347 |
if (has_password and
|
|
348 |
not self.cleaned_data.get('username') and
|
|
349 |
not self.cleaned_data.get('email')): |
|
363 |
if (has_password |
|
364 |
and not self.cleaned_data.get('username')
|
|
365 |
and not self.cleaned_data.get('email')):
|
|
350 | 366 |
raise forms.ValidationError( |
351 | 367 |
_('You must set a username or an email to set a password or send an activation link.')) |
352 | 368 | |
... | ... | |
383 | 399 |
return user |
384 | 400 | |
385 | 401 |
class Meta: |
386 |
model = get_user_model()
|
|
402 |
model = User
|
|
387 | 403 |
fields = '__all__' |
388 | 404 |
exclude = ('ou',) |
389 | 405 | |
... | ... | |
694 | 710 | |
695 | 711 |
class Meta: |
696 | 712 |
fields = () |
713 | ||
714 | ||
715 |
class SiteImportForm(forms.Form): |
|
716 |
site_json = forms.FileField( |
|
717 |
label=_('Site Export File')) |
src/authentic2/manager/ou_views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import json |
2 | 18 | |
3 | 19 |
from django_rbac.utils import get_ou_model |
src/authentic2/manager/resources.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.contrib.auth import get_user_model |
|
1 | 18 |
from django.utils import six |
19 | ||
2 | 20 |
from import_export.resources import ModelResource |
3 | 21 |
from import_export.fields import Field |
4 | 22 |
from import_export.widgets import Widget |
5 | 23 | |
6 |
from authentic2.compat import get_user_model |
|
7 | 24 |
from authentic2.a2_rbac.models import Role |
8 | 25 | |
26 |
User = get_user_model() |
|
27 | ||
9 | 28 | |
10 | 29 |
class ListWidget(Widget): |
11 | 30 |
def clean(self, value): |
... | ... | |
27 | 46 |
return ', '.join(map(six.text_type, result)) |
28 | 47 | |
29 | 48 |
class Meta: |
30 |
model = get_user_model()
|
|
49 |
model = User
|
|
31 | 50 |
exclude = ('password', 'user_permissions', 'is_staff', |
32 | 51 |
'is_superuser', 'groups') |
33 | 52 |
export_order = ('ou', 'uuid', 'id', 'username', 'email', |
src/authentic2/manager/role_views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import json |
2 | 18 | |
3 | 19 |
from django.core.exceptions import PermissionDenied |
4 | 20 |
from django.utils.translation import ugettext_lazy as _ |
5 |
from django.views.generic import ListView, FormView, TemplateView |
|
6 |
from django.views.generic.edit import FormMixin, DeleteView |
|
21 |
from django.views.generic import FormView, TemplateView |
|
7 | 22 |
from django.views.generic.detail import SingleObjectMixin |
8 | 23 |
from django.contrib import messages |
9 | 24 |
from django.contrib.contenttypes.models import ContentType |
10 | 25 |
from django.db.models.query import Q |
11 | 26 |
from django.db.models import Count |
12 | 27 |
from django.core.urlresolvers import reverse |
13 |
from django.http import Http404 |
|
14 | 28 |
from django.contrib.auth import get_user_model |
15 | 29 | |
16 |
from django_rbac.utils import get_role_model, get_permission_model, \ |
|
17 |
get_role_parenting_model, get_ou_model |
|
30 |
from django_rbac.utils import get_role_model, get_permission_model, get_ou_model |
|
18 | 31 | |
19 | 32 |
from authentic2.utils import redirect |
20 | 33 |
from authentic2 import hooks, data_transfer |
... | ... | |
38 | 51 |
# only non role-admin roles, they are accessed through the |
39 | 52 |
# RoleManager views |
40 | 53 |
if not self.admin_roles: |
41 |
qs = qs.filter(Q(admin_scope_ct__isnull=True) | |
|
42 |
Q(admin_scope_ct=permission_ct, |
|
43 |
admin_scope_id__in=permission_qs)) |
|
54 |
qs = qs.filter( |
|
55 |
Q(admin_scope_ct__isnull=True) | Q(admin_scope_ct=permission_ct, admin_scope_id__in=permission_qs)) |
|
44 | 56 |
if not self.service_roles: |
45 | 57 |
qs = qs.filter(service__isnull=True) |
46 | 58 |
return qs |
... | ... | |
177 | 189 |
def get_context_data(self, **kwargs): |
178 | 190 |
ctx = super(RoleMembersView, self).get_context_data(**kwargs) |
179 | 191 |
ctx['children'] = views.filter_view(self.request, |
180 |
self.object.children(include_self=False, |
|
181 |
annotate=True)) |
|
182 |
ctx['parents'] = views.filter_view(self.request, |
|
183 |
self.object.parents(include_self=False, |
|
184 |
annotate=True)) |
|
192 |
self.object.children(include_self=False, annotate=True)) |
|
193 |
ctx['parents'] = views.filter_view(self.request, self.object.parents(include_self=False, annotate=True)) |
|
185 | 194 |
ctx['admin_roles'] = views.filter_view(self.request, |
186 |
self.object.get_admin_role().children( |
|
187 |
include_self=False, annotate=True))
|
|
195 |
self.object.get_admin_role().children(include_self=False,
|
|
196 |
annotate=True))
|
|
188 | 197 |
return ctx |
189 | 198 | |
190 | 199 |
members = RoleMembersView.as_view() |
... | ... | |
373 | 382 | |
374 | 383 | |
375 | 384 |
class RoleAddAdminRoleView(views.AjaxFormViewMixin, views.TitleMixin, |
376 |
views.PermissionMixin, SingleObjectMixin, FormView): |
|
385 |
views.PermissionMixin, SingleObjectMixin, FormView):
|
|
377 | 386 |
title = _('Add admin role') |
378 | 387 |
model = get_role_model() |
379 | 388 |
form_class = forms.RolesForm |
... | ... | |
396 | 405 |
add_admin_role = RoleAddAdminRoleView.as_view() |
397 | 406 | |
398 | 407 | |
399 |
class RoleRemoveAdminRoleView(views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, |
|
400 |
views.PermissionMixin, TemplateView): |
|
408 |
class RoleRemoveAdminRoleView(views.TitleMixin, views.AjaxFormViewMixin, |
|
409 |
SingleObjectMixin, views.PermissionMixin, |
|
410 |
TemplateView): |
|
401 | 411 |
title = _('Remove admin role') |
402 | 412 |
model = get_role_model() |
403 | 413 |
success_url = '../..' |
... | ... | |
424 | 434 | |
425 | 435 | |
426 | 436 |
class RoleAddAdminUserView(views.AjaxFormViewMixin, views.TitleMixin, |
427 |
views.PermissionMixin, SingleObjectMixin, FormView): |
|
437 |
views.PermissionMixin, SingleObjectMixin, FormView):
|
|
428 | 438 |
title = _('Add admin user') |
429 | 439 |
model = get_role_model() |
430 | 440 |
form_class = forms.UsersForm |
... | ... | |
447 | 457 |
add_admin_user = RoleAddAdminUserView.as_view() |
448 | 458 | |
449 | 459 | |
450 |
class RoleRemoveAdminUserView(views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, |
|
451 |
views.PermissionMixin, TemplateView): |
|
460 |
class RoleRemoveAdminUserView(views.TitleMixin, views.AjaxFormViewMixin, |
|
461 |
SingleObjectMixin, views.PermissionMixin, |
|
462 |
TemplateView): |
|
452 | 463 |
title = _('Remove admin user') |
453 | 464 |
model = get_role_model() |
454 | 465 |
success_url = '../..' |
src/authentic2/manager/service_views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils import six |
2 | 18 |
from django.utils.translation import ugettext as _ |
3 | 19 |
from django.contrib import messages |
4 |
from django.shortcuts import get_object_or_404 |
|
5 | 20 | |
6 | 21 |
from authentic2.models import Service |
7 | 22 |
src/authentic2/manager/tables.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.contrib.auth import get_user_model |
|
1 | 18 |
from django.utils.translation import ugettext_lazy as _ |
2 |
from django.utils.safestring import mark_safe |
|
3 |
from django.contrib.auth.models import Group |
|
4 | 19 | |
5 | 20 |
import django_tables2 as tables |
6 | 21 |
from django_tables2.utils import A |
... | ... | |
9 | 24 |
get_ou_model |
10 | 25 | |
11 | 26 |
from authentic2.models import Service |
12 |
from authentic2.compat import get_user_model |
|
13 | 27 |
from authentic2.middleware import StoreRequestMiddleware |
14 | 28 | |
29 |
User = get_user_model() |
|
30 | ||
15 | 31 | |
16 | 32 |
class PermissionLinkColumn(tables.LinkColumn): |
17 | 33 |
def __init__(self, viewname, **kwargs): |
... | ... | |
39 | 55 |
ou = tables.Column() |
40 | 56 | |
41 | 57 |
class Meta: |
42 |
model = get_user_model()
|
|
58 |
model = User
|
|
43 | 59 |
attrs = {'class': 'main', 'id': 'user-table'} |
44 | 60 |
fields = ('username', 'email', 'first_name', |
45 | 61 |
'last_name', 'is_active', 'email_verified', 'ou') |
... | ... | |
100 | 116 |
via = tables.TemplateColumn( |
101 | 117 |
'''{% for rel in record.via %}{{ rel.child }} {% if not forloop.last %}, {% endif %}{% endfor %}''', |
102 | 118 |
verbose_name=_('Inherited from'), orderable=False) |
103 |
member = tables.TemplateColumn('''{% load i18n %}<input class="role-member{% if not record.member and record.via %} indeterminate{% endif %}" name='role-{{ record.pk }}' type='checkbox' {% if record.member %}checked{% endif %} {% if not record.has_perm %}disabled title="{% trans "You are not authorized to manage this role" %}"{% endif %}/>''', |
|
104 |
verbose_name=_('Member'), order_by=('member', 'via', 'name')) |
|
105 | ||
119 |
member = tables.TemplateColumn( |
|
120 |
'{% load i18n %}<input class="role-member{% if not record.member and record.via %} ' |
|
121 |
'indeterminate{% endif %}"' |
|
122 |
' name="role-{{ record.pk }}" type="checkbox" {% if record.member %}checked{% endif %} ' |
|
123 |
'{% if not record.has_perm %}disabled ' |
|
124 |
'title="{% trans "You are not authorized to manage this role" %}"{% endif %}/>', |
|
125 |
verbose_name=_('Member'), |
|
126 |
order_by=('member', 'via', 'name')) |
|
106 | 127 | |
107 | 128 |
class Meta: |
108 | 129 |
models = get_role_model() |
... | ... | |
117 | 138 |
accessor='name', verbose_name=_('label')) |
118 | 139 |
ou = tables.Column() |
119 | 140 |
via = tables.TemplateColumn( |
120 |
'''{% if not record.member %}{% for rel in record.child_relation.all %}{{ rel.child }} {% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}''', |
|
121 |
verbose_name=_('Inherited from'), orderable=False) |
|
141 |
'{% if not record.member %}{% for rel in record.child_relation.all %}' |
|
142 |
'{{ rel.child }} {% if not forloop.last %}, {% endif %}{% endfor %}' |
|
143 |
'{% endif %}', |
|
144 |
verbose_name=_('Inherited from'), |
|
145 |
orderable=False) |
|
122 | 146 | |
123 | 147 |
class Meta: |
124 | 148 |
models = get_role_model() |
src/authentic2/manager/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 | 18 | |
3 | 19 |
from django.views.i18n import javascript_catalog |
... | ... | |
127 | 143 |
) |
128 | 144 | |
129 | 145 |
urlpatterns += [ |
130 |
url(r'^jsi18n/$', javascript_catalog, |
|
131 |
{'packages': ('authentic2.manager',)}, |
|
132 |
name='a2-manager-javascript-catalog'), |
|
146 |
url(r'^jsi18n/$', |
|
147 |
javascript_catalog, |
|
148 |
{'packages': ('authentic2.manager',)}, |
|
149 |
name='a2-manager-javascript-catalog'), |
|
133 | 150 |
url(r'^select2.json$', views.select2, name='django_select2-json'), |
134 | 151 |
] |
src/authentic2/manager/user_views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import datetime |
2 |
import uuid |
|
3 | 18 |
import collections |
4 | 19 | |
5 | 20 |
from django.db import models |
6 | 21 |
from django.utils.translation import ugettext_lazy as _, ugettext |
7 |
from django.utils.http import urlsafe_base64_encode |
|
8 |
from django.utils.encoding import force_bytes |
|
9 | 22 |
from django.utils.html import format_html |
10 | 23 |
from django.core.mail import EmailMultiAlternatives |
11 | 24 |
from django.template import loader |
... | ... | |
13 | 26 |
from django.contrib.auth import get_user_model |
14 | 27 |
from django.contrib.contenttypes.models import ContentType |
15 | 28 |
from django.contrib import messages |
16 |
from django.http import HttpResponseRedirect, QueryDict |
|
17 |
from django.views.generic.detail import SingleObjectMixin |
|
18 |
from django.views.generic import View |
|
19 | 29 | |
20 |
from import_export.fields import Field |
|
21 | 30 |
import tablib |
22 | 31 | |
23 |
from authentic2.constants import SWITCH_USER_SESSION_KEY |
|
24 | 32 |
from authentic2.models import Attribute, AttributeValue, PasswordReset |
25 | 33 |
from authentic2.utils import switch_user, send_password_reset_mail, redirect, select_next_url |
26 | 34 |
from authentic2.a2_rbac.utils import get_default_ou |
... | ... | |
176 | 184 | |
177 | 185 |
def user_add_default_ou(request): |
178 | 186 |
ou = get_default_ou() |
179 |
return redirect(request, 'a2-manager-user-add', kwargs={'ou_pk': ou.id}, |
|
180 |
keep_params=True) |
|
187 |
return redirect(request, 'a2-manager-user-add', kwargs={'ou_pk': ou.id}, keep_params=True) |
|
181 | 188 | |
182 | 189 | |
183 | 190 |
class UserDetailView(OtherActionsMixin, BaseDetailView): |
... | ... | |
464 | 471 |
response = super(UserChangeEmailView, self).form_valid(form) |
465 | 472 |
new_email = form.cleaned_data['new_email'] |
466 | 473 |
hooks.call_hooks( |
467 |
'event',
|
|
468 |
name='manager-change-email-request',
|
|
469 |
user=self.request.user,
|
|
470 |
instance=form.instance,
|
|
471 |
form=form,
|
|
472 |
email=new_email)
|
|
474 |
'event', |
|
475 |
name='manager-change-email-request', |
|
476 |
user=self.request.user, |
|
477 |
instance=form.instance, |
|
478 |
form=form, |
|
479 |
email=new_email) |
|
473 | 480 |
return response |
474 | 481 | |
475 | 482 |
user_change_email = UserChangeEmailView.as_view() |
src/authentic2/manager/utils.py | ||
---|---|---|
1 | ||
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
3 | 17 |
from django_rbac.utils import get_ou_model |
4 | 18 |
src/authentic2/manager/views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import json |
2 | 18 |
import inspect |
3 | 19 | |
... | ... | |
25 | 41 |
from django_rbac.utils import get_ou_model |
26 | 42 | |
27 | 43 |
from authentic2.data_transfer import export_site, import_site, DataImportError, ImportContext |
28 |
from authentic2.forms import modelform_factory, SiteImportForm
|
|
44 |
from authentic2.forms.profile import modelform_factory
|
|
29 | 45 |
from authentic2.utils import redirect, batch_queryset |
30 | 46 |
from authentic2.decorators import json as json_view |
31 | 47 |
from authentic2 import hooks |
32 | 48 | |
33 |
from . import app_settings, utils |
|
49 |
from . import app_settings, utils, forms
|
|
34 | 50 | |
35 | 51 | |
36 | 52 |
# https://github.com/MongoEngine/django-mongoengine/blob/master/django_mongoengine/views/edit.py |
... | ... | |
643 | 659 |
def get_table(self, **kwargs): |
644 | 660 |
OU = get_ou_model() |
645 | 661 |
exclude_ou = False |
646 |
if (hasattr(self, 'search_form') and self.search_form.is_valid() and |
|
647 |
self.search_form.cleaned_data.get('ou') is not None): |
|
662 |
if (hasattr(self, 'search_form') |
|
663 |
and self.search_form.is_valid() |
|
664 |
and self.search_form.cleaned_data.get('ou') is not None): |
|
648 | 665 |
exclude_ou = True |
649 | 666 |
if OU.objects.count() < 2: |
650 | 667 |
exclude_ou = True |
... | ... | |
680 | 697 | |
681 | 698 | |
682 | 699 |
class SiteImportView(MediaMixin, FormView): |
683 |
form_class = SiteImportForm |
|
700 |
form_class = forms.SiteImportForm
|
|
684 | 701 |
template_name = 'authentic2/manager/site_import.html' |
685 | 702 |
success_url = reverse_lazy('a2-manager-homepage') |
686 | 703 |
src/authentic2/manager/widgets.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import operator |
2 | 18 | |
3 | 19 |
from django_select2.forms import ModelSelect2Widget, ModelSelect2MultipleWidget |
src/authentic2/managers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from datetime import timedelta |
2 | 18 |
import logging |
3 | 19 | |
... | ... | |
5 | 21 |
from django.db import models |
6 | 22 |
from django.db.models.query import QuerySet |
7 | 23 |
from django.utils.timezone import now |
8 |
from django.utils.http import urlquote |
|
9 | 24 |
from django.conf import settings |
10 | 25 |
from django.contrib.contenttypes.models import ContentType |
11 | 26 |
src/authentic2/middleware.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
import datetime |
3 | 19 |
import random |
... | ... | |
18 | 34 | |
19 | 35 |
from . import app_settings, utils, plugins |
20 | 36 | |
37 | ||
21 | 38 |
class ThreadCollector(object): |
22 | 39 |
def __init__(self): |
23 | 40 |
if threading is None: |
... | ... | |
48 | 65 | |
49 | 66 |
MESSAGE_IF_STRING_REPRESENTATION_INVALID = '[Could not get log message]' |
50 | 67 | |
68 | ||
51 | 69 |
class ThreadTrackingHandler(logging.Handler): |
52 | 70 |
def __init__(self, collector): |
53 | 71 |
logging.Handler.__init__(self) |
... | ... | |
77 | 95 |
logging_handler = ThreadTrackingHandler(collector) |
78 | 96 |
logging.root.addHandler(logging_handler) |
79 | 97 | |
98 | ||
80 | 99 |
class LoggingCollectorMiddleware(object): |
81 | 100 |
def process_request(self, request): |
82 | 101 |
collector.clear_collection() |
... | ... | |
90 | 109 |
request.logs = collector.get_collection() |
91 | 110 |
request.exception = exception |
92 | 111 | |
112 | ||
93 | 113 |
class CollectIPMiddleware(object): |
94 | 114 |
def process_response(self, request, response): |
95 | 115 |
# only collect IP if session is used |
... | ... | |
104 | 124 |
request.session.modified = True |
105 | 125 |
return response |
106 | 126 | |
127 | ||
107 | 128 |
class OpenedSessionCookieMiddleware(object): |
108 | 129 |
def process_response(self, request, response): |
109 | 130 |
# do not emit cookie for API requests |
... | ... | |
122 | 143 |
response.delete_cookie(name, domain=domain) |
123 | 144 |
return response |
124 | 145 | |
146 | ||
125 | 147 |
class RequestIdMiddleware(object): |
126 | 148 |
def process_request(self, request): |
127 | 149 |
if not hasattr(request, 'request_id'): |
... | ... | |
136 | 158 |
hexlify(struct.pack('I', random_id)), |
137 | 159 |
encoding='ascii') |
138 | 160 | |
161 | ||
139 | 162 |
class StoreRequestMiddleware(object): |
140 | 163 |
collection = {} |
141 | 164 | |
... | ... | |
153 | 176 |
def get_request(cls): |
154 | 177 |
return cls.collection.get(threading.currentThread()) |
155 | 178 | |
179 | ||
156 | 180 |
class ViewRestrictionMiddleware(object): |
157 | 181 |
RESTRICTION_SESSION_KEY = 'view-restriction' |
158 | 182 | |
... | ... | |
185 | 209 |
messages.warning(request, _('You must change your password to continue')) |
186 | 210 |
return utils.redirect_and_come_back(request, view) |
187 | 211 | |
212 | ||
188 | 213 |
class XForwardedForMiddleware(object): |
189 | 214 |
'''Copy the first address from X-Forwarded-For header to the REMOTE_ADDR meta. |
190 | 215 | |
... | ... | |
195 | 220 |
request.META['REMOTE_ADDR'] = request.META['HTTP_X_FORWARDED_FOR'].split(",")[0].strip() |
196 | 221 |
return None |
197 | 222 | |
223 | ||
198 | 224 |
class DisplayMessageBeforeRedirectMiddleware(object): |
199 | 225 |
'''Verify if messages are currently stored and if there is a redirection to another domain, in |
200 | 226 |
this case show an intermediate page. |
... | ... | |
236 | 262 | |
237 | 263 | |
238 | 264 |
class ServiceAccessControlMiddleware(object): |
239 | ||
240 | 265 |
def process_exception(self, request, exception): |
241 | 266 |
if not isinstance(exception, (utils.ServiceAccessDenied,)): |
242 | 267 |
return None |
src/authentic2/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import time |
2 | 18 |
import uuid |
3 | 19 |
from django.utils.http import urlquote |
... | ... | |
7 | 23 |
from django.utils import six |
8 | 24 |
from django.utils.translation import ugettext_lazy as _ |
9 | 25 |
from django.utils.six.moves.urllib import parse as urlparse |
10 |
from django.core.exceptions import ValidationError, FieldDoesNotExist
|
|
26 |
from django.core.exceptions import ValidationError |
|
11 | 27 |
from django.contrib.contenttypes.models import ContentType |
12 | 28 | |
13 | 29 |
from model_utils.managers import QueryManager |
14 | 30 | |
15 | 31 |
from authentic2.a2_rbac.models import Role |
16 |
from authentic2.a2_rbac.utils import get_default_ou |
|
17 | 32 |
from django_rbac.utils import get_role_model_name |
18 | 33 | |
19 | 34 |
try: |
20 | 35 |
from django.contrib.contenttypes.fields import GenericForeignKey |
21 | 36 |
except ImportError: |
22 | 37 |
from django.contrib.contenttypes.generic import GenericForeignKey |
23 |
from django.contrib.contenttypes.models import ContentType |
|
24 | 38 | |
25 | 39 |
from . import managers |
26 | 40 |
# install our natural_key implementation |
27 |
from . import natural_key |
|
41 |
from . import natural_key as unused_natural_key # noqa: F401
|
|
28 | 42 |
from .utils import ServiceAccessDenied |
29 | 43 | |
30 | 44 | |
... | ... | |
33 | 47 | |
34 | 48 |
objects = managers.DeletedUserManager() |
35 | 49 | |
36 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, |
|
37 |
verbose_name=_('user')) |
|
38 |
creation = models.DateTimeField(auto_now_add=True, |
|
39 |
verbose_name=_('creation date')) |
|
50 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user')) |
|
51 |
creation = models.DateTimeField(auto_now_add=True, verbose_name=_('creation date')) |
|
40 | 52 | |
41 | 53 |
class Meta: |
42 | 54 |
verbose_name = _('user to delete') |
43 | 55 |
verbose_name_plural = _('users to delete') |
44 | 56 | |
57 | ||
45 | 58 |
@six.python_2_unicode_compatible |
46 | 59 |
class UserExternalId(models.Model): |
47 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, |
|
48 |
verbose_name=_('user')) |
|
49 |
source = models.CharField(max_length=256, |
|
50 |
verbose_name=_('source')) |
|
51 |
external_id = models.CharField(max_length=256, |
|
52 |
verbose_name=_('external id')) |
|
53 |
created = models.DateTimeField(auto_now_add=True, |
|
54 |
verbose_name=_('creation date')) |
|
55 |
updated = models.DateTimeField(auto_now=True, |
|
56 |
verbose_name=_('last update date')) |
|
60 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user')) |
|
61 |
source = models.CharField(max_length=256, verbose_name=_('source')) |
|
62 |
external_id = models.CharField(max_length=256, verbose_name=_('external id')) |
|
63 |
created = models.DateTimeField(auto_now_add=True, verbose_name=_('creation date')) |
|
64 |
updated = models.DateTimeField(auto_now=True, verbose_name=_('last update date')) |
|
57 | 65 | |
58 | 66 |
def __str__(self): |
59 |
return u'{0} is {1} on {2}'.format( |
|
60 |
self.user, self.external_id, self.source) |
|
67 |
return u'{0} is {1} on {2}'.format(self.user, self.external_id, self.source) |
|
61 | 68 | |
62 | 69 |
def __repr__(self): |
63 | 70 |
return '<UserExternalId user: {0!r} source: {1!r} ' \ |
... | ... | |
69 | 76 |
verbose_name = _('user external id') |
70 | 77 |
verbose_name_plural = _('user external ids') |
71 | 78 | |
79 | ||
72 | 80 |
@six.python_2_unicode_compatible |
73 | 81 |
class AuthenticationEvent(models.Model): |
74 | 82 |
'''Record authentication events whatever the source''' |
75 |
when = models.DateTimeField(auto_now=True, |
|
76 |
verbose_name=_('when')) |
|
77 |
who = models.CharField(max_length=80, |
|
78 |
verbose_name=_('who')) |
|
79 |
how = models.CharField(max_length=32, |
|
80 |
verbose_name=_('how')) |
|
81 |
nonce = models.CharField(max_length=255, |
|
82 |
verbose_name=_('nonce')) |
|
83 |
when = models.DateTimeField(auto_now=True, verbose_name=_('when')) |
|
84 |
who = models.CharField(max_length=80, verbose_name=_('who')) |
|
85 |
how = models.CharField(max_length=32, verbose_name=_('how')) |
|
86 |
nonce = models.CharField(max_length=255, verbose_name=_('nonce')) |
|
83 | 87 | |
84 | 88 |
objects = managers.AuthenticationEventManager() |
85 | 89 | |
... | ... | |
91 | 95 |
return _('Authentication of %(who)s by %(how)s at %(when)s') % \ |
92 | 96 |
self.__dict__ |
93 | 97 | |
98 | ||
94 | 99 |
class LogoutUrlAbstract(models.Model): |
95 |
logout_url = models.URLField(verbose_name=_('url'), help_text=_('you can use a {} ' |
|
96 |
'to pass the URL of the success icon, ex.: ' |
|
97 |
'http://example.com/logout?next={}'), max_length=255, blank=True, null=True) |
|
100 |
logout_url = models.URLField( |
|
101 |
verbose_name=_('url'), |
|
102 |
help_text=_('you can use a {} to pass the URL of the success icon, ' |
|
103 |
'ex.: http://example.com/logout?next={}'), |
|
104 |
max_length=255, |
|
105 |
blank=True, |
|
106 |
null=True) |
|
98 | 107 |
logout_use_iframe = models.BooleanField( |
99 |
verbose_name=_('use an iframe instead of an img tag for logout'),
|
|
100 |
default=False)
|
|
108 |
verbose_name=_('use an iframe instead of an img tag for logout'), |
|
109 |
default=False) |
|
101 | 110 |
logout_use_iframe_timeout = models.PositiveIntegerField( |
102 |
verbose_name=_('iframe logout timeout (ms)'),
|
|
103 |
help_text=_('if iframe logout is used, it\'s the time between the '
|
|
104 |
'onload event for this iframe and the moment we consider its ' |
|
105 |
'loading to be really finished'), |
|
106 |
default=300)
|
|
111 |
verbose_name=_('iframe logout timeout (ms)'), |
|
112 |
help_text=_('if iframe logout is used, it\'s the time between the ' |
|
113 |
'onload event for this iframe and the moment we consider its '
|
|
114 |
'loading to be really finished'),
|
|
115 |
default=300) |
|
107 | 116 | |
108 | 117 |
def get_logout_url(self, request): |
109 |
ok_icon_url = request.build_absolute_uri(urlparse.urljoin(settings.STATIC_URL, |
|
110 |
'authentic2/images/ok.png')) + '?nonce=%s' % time.time() |
|
118 |
ok_icon_url = ( |
|
119 |
request.build_absolute_uri(urlparse.urljoin(settings.STATIC_URL, 'authentic2/images/ok.png')) |
|
120 |
+ '?nonce=%s' % time.time()) |
|
111 | 121 |
return self.logout_url.format(urlquote(ok_icon_url)) |
112 | 122 | |
113 | 123 |
class Meta: |
... | ... | |
115 | 125 | |
116 | 126 | |
117 | 127 |
class LogoutUrl(LogoutUrlAbstract): |
118 |
content_type = models.ForeignKey(ContentType, |
|
119 |
verbose_name=_('content type')) |
|
120 |
object_id = models.PositiveIntegerField( |
|
121 |
verbose_name=_('object identifier')) |
|
128 |
content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) |
|
129 |
object_id = models.PositiveIntegerField(verbose_name=_('object identifier')) |
|
122 | 130 |
provider = GenericForeignKey('content_type', 'object_id') |
123 | 131 | |
124 | 132 |
class Meta: |
... | ... | |
128 | 136 | |
129 | 137 |
@six.python_2_unicode_compatible |
130 | 138 |
class Attribute(models.Model): |
131 |
label = models.CharField(verbose_name=_('label'), max_length=63, |
|
132 |
unique=True) |
|
139 |
label = models.CharField(verbose_name=_('label'), max_length=63, unique=True) |
|
133 | 140 |
description = models.TextField(verbose_name=_('description'), blank=True) |
134 |
name = models.SlugField(verbose_name=_('name'), max_length=256, |
|
135 |
unique=True) |
|
136 |
required = models.BooleanField( |
|
137 |
verbose_name=_('required'), |
|
138 |
blank=True, default=False) |
|
139 |
asked_on_registration = models.BooleanField( |
|
140 |
verbose_name=_('asked on registration'), |
|
141 |
blank=True, default=False) |
|
142 |
user_editable = models.BooleanField( |
|
143 |
verbose_name=_('user editable'), |
|
144 |
blank=True, default=False) |
|
145 |
user_visible = models.BooleanField( |
|
146 |
verbose_name=_('user visible'), |
|
147 |
blank=True, default=False) |
|
148 |
multiple = models.BooleanField( |
|
149 |
verbose_name=_('multiple'), |
|
150 |
blank=True, default=False) |
|
151 |
kind = models.CharField(max_length=16, |
|
152 |
verbose_name=_('kind')) |
|
153 |
disabled = models.BooleanField(verbose_name=_('disabled'), |
|
154 |
blank=True, default=False) |
|
155 |
searchable = models.BooleanField( |
|
156 |
verbose_name=_('searchable'), |
|
157 |
blank=True, default=False) |
|
141 |
name = models.SlugField(verbose_name=_('name'), max_length=256, unique=True) |
|
142 |
required = models.BooleanField(verbose_name=_('required'), blank=True, default=False) |
|
143 |
asked_on_registration = models.BooleanField(verbose_name=_('asked on registration'), blank=True, default=False) |
|
144 |
user_editable = models.BooleanField(verbose_name=_('user editable'), blank=True, default=False) |
|
145 |
user_visible = models.BooleanField(verbose_name=_('user visible'), blank=True, default=False) |
|
146 |
multiple = models.BooleanField(verbose_name=_('multiple'), blank=True, default=False) |
|
147 |
kind = models.CharField(max_length=16, verbose_name=_('kind')) |
|
148 |
disabled = models.BooleanField(verbose_name=_('disabled'), blank=True, default=False) |
|
149 |
searchable = models.BooleanField(verbose_name=_('searchable'), blank=True, default=False) |
|
158 | 150 | |
159 | 151 |
scopes = models.CharField( |
160 | 152 |
verbose_name=_('scopes'), |
... | ... | |
220 | 212 |
for value in values: |
221 | 213 |
content = serialize(value) |
222 | 214 |
av, created = AttributeValue.objects.get_or_create( |
223 |
content_type=ContentType.objects.get_for_model(owner),
|
|
224 |
object_id=owner.pk,
|
|
225 |
attribute=self,
|
|
226 |
multiple=True,
|
|
227 |
content=content,
|
|
228 |
defaults={'verified': verified})
|
|
215 |
content_type=ContentType.objects.get_for_model(owner), |
|
216 |
object_id=owner.pk, |
|
217 |
attribute=self, |
|
218 |
multiple=True, |
|
219 |
content=content, |
|
220 |
defaults={'verified': verified}) |
|
229 | 221 |
if not created: |
230 | 222 |
av.verified = verified |
231 | 223 |
av.save() |
... | ... | |
237 | 229 |
av, created = attribute_value, False |
238 | 230 |
else: |
239 | 231 |
av, created = AttributeValue.objects.get_or_create( |
240 |
content_type=ContentType.objects.get_for_model(owner),
|
|
241 |
object_id=owner.pk,
|
|
242 |
attribute=self,
|
|
243 |
multiple=False,
|
|
244 |
defaults={'content': content, 'verified': verified})
|
|
232 |
content_type=ContentType.objects.get_for_model(owner), |
|
233 |
object_id=owner.pk, |
|
234 |
attribute=self, |
|
235 |
multiple=False, |
|
236 |
defaults={'content': content, 'verified': verified}) |
|
245 | 237 |
if not created and (av.content != content or av.verified != verified): |
246 | 238 |
av.content = content |
247 | 239 |
av.verified = verified |
... | ... | |
261 | 253 | |
262 | 254 | |
263 | 255 |
class AttributeValue(models.Model): |
264 |
content_type = models.ForeignKey('contenttypes.ContentType', |
|
265 |
verbose_name=_('content type')) |
|
266 |
object_id = models.PositiveIntegerField( |
|
267 |
verbose_name=_('object identifier'), |
|
268 |
db_index=True) |
|
256 |
content_type = models.ForeignKey('contenttypes.ContentType', verbose_name=_('content type')) |
|
257 |
object_id = models.PositiveIntegerField(verbose_name=_('object identifier'), db_index=True) |
|
269 | 258 |
owner = GenericForeignKey('content_type', 'object_id') |
270 | 259 | |
271 |
attribute = models.ForeignKey('Attribute', |
|
272 |
verbose_name=_('attribute')) |
|
260 |
attribute = models.ForeignKey( |
|
261 |
'Attribute', |
|
262 |
verbose_name=_('attribute')) |
|
273 | 263 |
multiple = models.BooleanField(default=False) |
274 | 264 | |
275 | 265 |
content = models.TextField(verbose_name=_('content'), db_index=True) |
... | ... | |
297 | 287 | |
298 | 288 |
@six.python_2_unicode_compatible |
299 | 289 |
class PasswordReset(models.Model): |
300 |
user = models.OneToOneField(settings.AUTH_USER_MODEL, |
|
301 |
verbose_name=_('user')) |
|
290 |
user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_('user')) |
|
302 | 291 | |
303 | 292 |
def save(self, *args, **kwargs): |
304 | 293 |
if self.user_id and not self.user.has_usable_password(): |
... | ... | |
358 | 347 |
verbose_name = _('base service model') |
359 | 348 |
verbose_name_plural = _('base service models') |
360 | 349 |
unique_together = ( |
361 |
('slug', 'ou'),
|
|
350 |
('slug', 'ou'), |
|
362 | 351 |
) |
363 | 352 | |
364 | 353 |
def natural_key(self): |
... | ... | |
393 | 382 |
def to_json(self, roles=None): |
394 | 383 |
if roles is None: |
395 | 384 |
roles = Role.objects.all() |
396 |
roles = roles.filter(Q(service=self)|Q(ou=self.ou, service__isnull=True))
|
|
385 |
roles = roles.filter(Q(service=self) | Q(ou=self.ou, service__isnull=True))
|
|
397 | 386 |
return { |
398 | 387 |
'name': self.name, |
399 | 388 |
'slug': self.slug, |
src/authentic2/natural_key.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db import models |
2 | 18 | |
3 | 19 |
from django.contrib.contenttypes.models import ContentType |
src/authentic2/nonce/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from authentic2.nonce.utils import accept_nonce, cleanup_nonces |
2 | 18 | |
3 | 19 |
__all__ = ('accept_nonce', 'cleanup_nonces') |
src/authentic2/nonce/models.py | ||
---|---|---|
1 |
import datetime as dt |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
3 | 17 |
from django.db import models |
4 | 18 |
from django.utils import timezone, six |
... | ... | |
7 | 21 | |
8 | 22 |
_NONCE_LENGTH_CONSTANT = 256 |
9 | 23 | |
24 | ||
10 | 25 |
class NonceManager(models.Manager): |
11 | 26 |
def cleanup(self, now=None): |
12 | 27 |
now = now or timezone.now() |
13 | 28 |
self.filter(not_on_or_after__lt=now).delete() |
14 | 29 | |
30 | ||
15 | 31 |
@six.python_2_unicode_compatible |
16 | 32 |
class Nonce(models.Model): |
17 | 33 |
value = models.CharField(max_length=_NONCE_LENGTH_CONSTANT) |
18 |
context = models.CharField(max_length=_NONCE_LENGTH_CONSTANT, blank=True, |
|
19 |
null=True) |
|
34 |
context = models.CharField(max_length=_NONCE_LENGTH_CONSTANT, blank=True, null=True) |
|
20 | 35 |
not_on_or_after = models.DateTimeField(blank=True, null=True) |
21 | 36 | |
22 |
objects = NonceManager()
|
|
37 |
objects = NonceManager() |
|
23 | 38 | |
24 | 39 |
def __str__(self): |
25 | 40 |
return self.value |
src/authentic2/nonce/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import os.path |
2 | 18 |
import datetime as dt |
3 | 19 |
from calendar import timegm |
... | ... | |
12 | 28 |
STORAGE_MODEL = 'model' |
13 | 29 |
STORAGE_FILESYSTEM = 'fs:' |
14 | 30 | |
31 | ||
15 | 32 |
def compute_not_on_or_after(now, not_on_or_after): |
16 |
try: # first try integer semantic |
|
33 |
try: # first try integer semantic
|
|
17 | 34 |
seconds = int(not_on_or_after) |
18 | 35 |
not_on_or_after = now + dt.timedelta(seconds=seconds) |
19 | 36 |
except ValueError: |
20 |
try: # try timedelta semantic |
|
37 |
try: # try timedelta semantic
|
|
21 | 38 |
not_on_or_after = now + not_on_or_after |
22 |
except TypeError: # datetime semantic |
|
39 |
except TypeError: # datetime semantic
|
|
23 | 40 |
pass |
24 | 41 |
return not_on_or_after |
25 | 42 | |
... | ... | |
28 | 45 |
# condition errors. But any other OSError is problematic and should be |
29 | 46 |
# reported to the administrator by mail and so we let it unroll the stack |
30 | 47 | |
48 | ||
31 | 49 |
def unlink_if_exists(path): |
32 | 50 |
try: |
33 | 51 |
os.unlink(path) |
... | ... | |
35 | 53 |
if e.errno != errno.ENOENT: |
36 | 54 |
raise |
37 | 55 | |
38 |
def accept_nonce_file_storage(path, now, value, context=None, |
|
39 |
not_on_or_after=None):
|
|
56 | ||
57 |
def accept_nonce_file_storage(path, now, value, context=None, not_on_or_after=None):
|
|
40 | 58 |
''' |
41 | 59 |
Use a directory as a storage for nonce-context values. The last |
42 | 60 |
modification time is used to store the expiration timestamp. |
... | ... | |
81 | 99 |
return False |
82 | 100 |
return True |
83 | 101 | |
102 | ||
84 | 103 |
def accept_nonce_model(now, value, context=None, not_on_or_after=None): |
85 | 104 |
import models |
86 | 105 | |
87 | 106 |
if not_on_or_after: |
88 | 107 |
not_on_or_after = compute_not_on_or_after(now, not_on_or_after) |
89 |
nonce, created = models.Nonce.objects.get_or_create(value=value, |
|
90 |
context=context) |
|
108 |
nonce, created = models.Nonce.objects.get_or_create(value=value, context=context) |
|
91 | 109 |
if created or (nonce.not_on_or_after and nonce.not_on_or_after < now): |
92 | 110 |
nonce.not_on_or_after = not_on_or_after |
93 | 111 |
nonce.save() |
... | ... | |
95 | 113 |
else: |
96 | 114 |
return False |
97 | 115 | |
116 | ||
98 | 117 |
def cleanup_nonces_file_storage(dir_path, now): |
99 | 118 |
for nonce_path in glob.iglob(os.path.join(dir_path, '*')): |
100 | 119 |
now_time = timegm(now.utctimetuple()) |
... | ... | |
112 | 131 |
continue |
113 | 132 |
raise |
114 | 133 | |
134 | ||
115 | 135 |
def cleanup_nonces(now=None): |
116 | 136 |
''' |
117 | 137 |
Cleanup stored nonce whose timestamp has expired, i.e. |
... | ... | |
135 | 155 |
else: |
136 | 156 |
raise ValueError('Invalid NONCE_STORAGE setting: %r' % mode) |
137 | 157 | |
158 | ||
138 | 159 |
def accept_nonce(value, context=None, not_on_or_after=None, now=None): |
139 | 160 |
''' |
140 | 161 |
Verify that the given nonce value has not already been seen in the |
... | ... | |
167 | 188 |
now = now or dt.datetime.now() |
168 | 189 |
mode = getattr(settings, 'NONCE_STORAGE', STORAGE_MODEL) |
169 | 190 |
if mode == STORAGE_MODEL: |
170 |
return accept_nonce_model(now, value, context=context, |
|
171 |
not_on_or_after=not_on_or_after) |
|
191 |
return accept_nonce_model(now, value, context=context, not_on_or_after=not_on_or_after) |
|
172 | 192 |
elif mode.startswith(STORAGE_FILESYSTEM): |
173 | 193 |
dir_path = mode[len(STORAGE_FILESYSTEM):] |
174 |
return accept_nonce_file_storage(dir_path, now, value, |
|
175 |
context=context, not_on_or_after=not_on_or_after) |
|
194 |
return accept_nonce_file_storage(dir_path, now, value, context=context, not_on_or_after=not_on_or_after) |
|
176 | 195 |
else: |
177 | 196 |
raise ValueError('Invalid NONCE_STORAGE setting: %r' % mode) |
src/authentic2/passwords.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import string |
2 | 18 |
import random |
3 | 19 |
import re |
... | ... | |
6 | 22 |
from django.utils.translation import ugettext as _ |
7 | 23 |
from django.utils.module_loading import import_string |
8 | 24 |
from django.utils.functional import lazy |
9 |
from django.utils.safestring import mark_safe |
|
10 | 25 |
from django.utils import six |
11 | 26 |
from django.core.exceptions import ValidationError |
12 | 27 | |
28 | ||
13 | 29 |
from . import app_settings |
14 | 30 | |
15 | 31 |
src/authentic2/plugins.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
""" |
2 | 18 |
Use setuptools entrypoints to find plugins |
3 | 19 | |
... | ... | |
17 | 33 | |
18 | 34 |
PLUGIN_CACHE = {} |
19 | 35 | |
36 | ||
20 | 37 |
class PluginError(Exception): |
21 | 38 |
pass |
22 | 39 | |
23 | 40 |
DEFAULT_GROUP_NAME = 'authentic2.plugin' |
24 | 41 | |
42 | ||
25 | 43 |
def get_plugins(group_name=DEFAULT_GROUP_NAME, use_cache=True, *args, **kwargs): |
26 | 44 |
'''Traverse all entry points for group_name and instantiate them using args |
27 | 45 |
and kwargs. |
... | ... | |
40 | 58 |
PLUGIN_CACHE[group_name] = plugins |
41 | 59 |
return plugins |
42 | 60 | |
43 |
def register_plugins_urls(urlpatterns, |
|
44 |
group_name=DEFAULT_GROUP_NAME):
|
|
61 | ||
62 |
def register_plugins_urls(urlpatterns, group_name=DEFAULT_GROUP_NAME):
|
|
45 | 63 |
'''Call get_before_urls and get_after_urls on all plugins providing them |
46 | 64 |
and add those urls to the given urlpatterns. |
47 | 65 | |
... | ... | |
62 | 80 | |
63 | 81 |
return before_urls + urlpatterns + after_urls |
64 | 82 | |
83 | ||
65 | 84 |
def register_plugins_installed_apps(installed_apps, group_name=DEFAULT_GROUP_NAME): |
66 | 85 |
'''Call get_apps() on all plugins of group_name and add the returned |
67 | 86 |
applications path to the installed_apps sequence. |
... | ... | |
77 | 96 |
installed_apps.append(app) |
78 | 97 |
return installed_apps |
79 | 98 | |
80 |
def register_plugins_middleware(middleware_classes, |
|
81 |
group_name=DEFAULT_GROUP_NAME):
|
|
99 | ||
100 |
def register_plugins_middleware(middleware_classes, group_name=DEFAULT_GROUP_NAME):
|
|
82 | 101 |
middleware_classes = list(middleware_classes) |
83 | 102 |
for plugin in get_plugins(group_name): |
84 | 103 |
if hasattr(plugin, 'get_before_middleware'): |
... | ... | |
93 | 112 |
middleware_classes.append(app) |
94 | 113 |
return tuple(middleware_classes) |
95 | 114 | |
96 |
def register_plugins_authentication_backends(authentication_backends, |
|
97 |
group_name=DEFAULT_GROUP_NAME):
|
|
115 | ||
116 |
def register_plugins_authentication_backends(authentication_backends, group_name=DEFAULT_GROUP_NAME):
|
|
98 | 117 |
authentication_backends = list(authentication_backends) |
99 | 118 |
for plugin in get_plugins(group_name): |
100 | 119 |
if hasattr(plugin, 'get_authentication_backends'): |
... | ... | |
104 | 123 |
authentication_backends.append(cls) |
105 | 124 |
return tuple(authentication_backends) |
106 | 125 | |
107 |
def register_plugins_authenticators(authenticators=(), |
|
108 |
group_name=DEFAULT_GROUP_NAME):
|
|
126 | ||
127 |
def register_plugins_authenticators(authenticators=(), group_name=DEFAULT_GROUP_NAME):
|
|
109 | 128 |
authenticators = list(authenticators) |
110 | 129 |
for plugin in get_plugins(group_name): |
111 | 130 |
if hasattr(plugin, 'get_authenticators'): |
... | ... | |
115 | 134 |
authenticators.append(cls) |
116 | 135 |
return tuple(authenticators) |
117 | 136 | |
118 |
def register_plugins_idp_backends(idp_backends, |
|
119 |
group_name=DEFAULT_GROUP_NAME):
|
|
137 | ||
138 |
def register_plugins_idp_backends(idp_backends, group_name=DEFAULT_GROUP_NAME):
|
|
120 | 139 |
idp_backends = list(idp_backends) |
121 | 140 |
for plugin in get_plugins(group_name): |
122 | 141 |
if hasattr(plugin, 'get_idp_backends'): |
src/authentic2/profile_forms.py | ||
---|---|---|
1 |
import logging |
|
2 | ||
3 |
from django import forms |
|
4 |
from django.utils.translation import ugettext as _ |
|
5 |
from django.contrib.auth import get_user_model |
|
6 | ||
7 |
from .backends import get_user_queryset |
|
8 |
from .utils import send_password_reset_mail |
|
9 |
from . import hooks, app_settings |
|
10 | ||
11 | ||
12 |
logger = logging.getLogger(__name__) |
|
13 | ||
14 | ||
15 |
class PasswordResetForm(forms.Form): |
|
16 |
next_url = forms.CharField(widget=forms.HiddenInput, required=False) |
|
17 | ||
18 |
email = forms.EmailField( |
|
19 |
label=_("Email"), max_length=254) |
|
20 | ||
21 |
def save(self): |
|
22 |
""" |
|
23 |
Generates a one-use only link for resetting password and sends to the |
|
24 |
user. |
|
25 |
""" |
|
26 |
email = self.cleaned_data["email"].strip() |
|
27 |
users = get_user_queryset() |
|
28 |
active_users = users.filter(email__iexact=email, is_active=True) |
|
29 |
for user in active_users: |
|
30 |
# we don't set the password to a random string, as some users should not have |
|
31 |
# a password |
|
32 |
set_random_password = (user.has_usable_password() |
|
33 |
and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET) |
|
34 |
send_password_reset_mail(user, set_random_password=set_random_password, |
|
35 |
next_url=self.cleaned_data.get('next_url')) |
|
36 |
if not active_users: |
|
37 |
logger.info(u'password reset requests for "%s", no user found') |
|
38 |
hooks.call_hooks('event', name='password-reset', email=email, users=active_users) |
src/authentic2/profile_urls.py | ||
---|---|---|
1 |
from django.conf.urls import url |
|
2 |
from django.contrib.auth import views as auth_views, REDIRECT_FIELD_NAME |
|
3 |
from django.contrib.auth.decorators import login_required |
|
4 |
from django.core.urlresolvers import reverse |
|
5 |
from django.http import HttpResponseRedirect |
|
6 |
from django.contrib import messages |
|
7 |
from django.utils.translation import ugettext as _ |
|
8 |
from django.views.decorators.debug import sensitive_post_parameters |
|
9 | ||
10 |
from authentic2.utils import import_module_or_class, redirect, user_can_change_password |
|
11 |
from . import app_settings, decorators, profile_views, hooks |
|
12 |
from .views import (logged_in, edit_profile, email_change, email_change_verify, profile) |
|
13 | ||
14 |
SET_PASSWORD_FORM_CLASS = import_module_or_class( |
|
15 |
app_settings.A2_REGISTRATION_SET_PASSWORD_FORM_CLASS) |
|
16 |
CHANGE_PASSWORD_FORM_CLASS = import_module_or_class( |
|
17 |
app_settings.A2_REGISTRATION_CHANGE_PASSWORD_FORM_CLASS) |
|
18 | ||
19 |
@sensitive_post_parameters() |
|
20 |
@login_required |
|
21 |
@decorators.setting_enabled('A2_REGISTRATION_CAN_CHANGE_PASSWORD') |
|
22 |
def password_change_view(request, *args, **kwargs): |
|
23 |
post_change_redirect = kwargs.pop('post_change_redirect', None) |
|
24 |
if 'next_url' in request.POST and request.POST['next_url']: |
|
25 |
post_change_redirect = request.POST['next_url'] |
|
26 |
elif REDIRECT_FIELD_NAME in request.GET: |
|
27 |
post_change_redirect = request.GET[REDIRECT_FIELD_NAME] |
|
28 |
elif post_change_redirect is None: |
|
29 |
post_change_redirect = reverse('account_management') |
|
30 |
if not user_can_change_password(request=request): |
|
31 |
messages.warning(request, _('Password change is forbidden')) |
|
32 |
return redirect(request, post_change_redirect) |
|
33 |
if 'cancel' in request.POST: |
|
34 |
return redirect(request, post_change_redirect) |
|
35 |
kwargs['post_change_redirect'] = post_change_redirect |
|
36 |
extra_context = kwargs.setdefault('extra_context', {}) |
|
37 |
extra_context['view'] = password_change_view |
|
38 |
extra_context[REDIRECT_FIELD_NAME] = post_change_redirect |
|
39 |
if not request.user.has_usable_password(): |
|
40 |
kwargs['password_change_form'] = SET_PASSWORD_FORM_CLASS |
|
41 |
response = auth_views.password_change(request, *args, **kwargs) |
|
42 |
if isinstance(response, HttpResponseRedirect): |
|
43 |
hooks.call_hooks('event', name='change-password', user=request.user, request=request) |
|
44 |
messages.info(request, _('Password changed')) |
|
45 |
return response |
|
46 | ||
47 |
password_change_view.title = _('Password Change') |
|
48 |
password_change_view.do_not_call_in_templates = True |
|
49 | ||
50 | ||
51 |
urlpatterns = [ |
|
52 |
url(r'^logged-in/$', logged_in, name='logged-in'), |
|
53 |
url(r'^edit/$', edit_profile, name='profile_edit'), |
|
54 |
url(r'^edit/(?P<scope>[-\w]+)/$', edit_profile, name='profile_edit_with_scope'), |
|
55 |
url(r'^change-email/$', email_change, name='email-change'), |
|
56 |
url(r'^change-email/verify/$', email_change_verify, |
|
57 |
name='email-change-verify'), |
|
58 |
url(r'^$', profile, name='account_management'), |
|
59 |
url(r'^password/change/$', |
|
60 |
password_change_view, |
|
61 |
{'password_change_form': CHANGE_PASSWORD_FORM_CLASS}, |
|
62 |
name='password_change'), |
|
63 |
url(r'^password/change/done/$', |
|
64 |
auth_views.password_change_done, |
|
65 |
name='password_change_done'), |
|
66 | ||
67 |
# Password reset |
|
68 |
url(r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
|
69 |
profile_views.password_reset_confirm, |
|
70 |
name='password_reset_confirm'), |
|
71 |
url(r'^password/reset/$', |
|
72 |
profile_views.password_reset, |
|
73 |
name='password_reset'), |
|
74 | ||
75 |
# Legacy |
|
76 |
url(r'^password/change/$', |
|
77 |
password_change_view, |
|
78 |
{'password_change_form': CHANGE_PASSWORD_FORM_CLASS}, |
|
79 |
name='auth_password_change'), |
|
80 |
url(r'^password/change/done/$', |
|
81 |
auth_views.password_change_done, |
|
82 |
name='auth_password_change_done'), |
|
83 |
url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
|
84 |
auth_views.password_reset_confirm, |
|
85 |
{'set_password_form': SET_PASSWORD_FORM_CLASS}, |
|
86 |
name='auth_password_reset_confirm'), |
|
87 |
url(r'^password/reset/$', |
|
88 |
auth_views.password_reset, |
|
89 |
name='auth_password_reset'), |
|
90 |
url(r'^password/reset/complete/$', |
|
91 |
auth_views.password_reset_complete, |
|
92 |
name='auth_password_reset_complete'), |
|
93 |
url(r'^password/reset/done/$', |
|
94 |
auth_views.password_reset_done, |
|
95 |
name='auth_password_reset_done'), |
|
96 |
url(r'^switch-back/$', profile_views.switch_back, name='a2-switch-back'), |
|
97 |
] |
src/authentic2/profile_views.py | ||
---|---|---|
1 |
import logging |
|
2 | ||
3 |
from django.views.generic import FormView |
|
4 |
from django.contrib import messages |
|
5 |
from django.contrib.auth import get_user_model, REDIRECT_FIELD_NAME, authenticate |
|
6 |
from django.http import Http404 |
|
7 |
from django.utils.translation import ugettext as _ |
|
8 |
from django.utils.http import urlsafe_base64_decode |
|
9 | ||
10 |
from .compat import default_token_generator |
|
11 |
from .registration_backend.forms import SetPasswordForm |
|
12 |
from . import app_settings, cbv, profile_forms, utils, hooks |
|
13 | ||
14 | ||
15 |
class PasswordResetView(cbv.NextURLViewMixin, FormView): |
|
16 |
'''Ask for an email and send a password reset link by mail''' |
|
17 |
form_class = profile_forms.PasswordResetForm |
|
18 |
title = _('Password Reset') |
|
19 | ||
20 |
def get_template_names(self): |
|
21 |
return [ |
|
22 |
'authentic2/password_reset_form.html', |
|
23 |
'registration/password_reset_form.html', |
|
24 |
] |
|
25 | ||
26 |
def get_form_kwargs(self, **kwargs): |
|
27 |
kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs) |
|
28 |
initial = kwargs.setdefault('initial', {}) |
|
29 |
initial['next_url'] = self.request.GET.get(REDIRECT_FIELD_NAME, '') |
|
30 |
return kwargs |
|
31 | ||
32 |
def get_context_data(self, **kwargs): |
|
33 |
ctx = super(PasswordResetView, self).get_context_data(**kwargs) |
|
34 |
if app_settings.A2_USER_CAN_RESET_PASSWORD is False: |
|
35 |
raise Http404('Password reset is not allowed.') |
|
36 |
ctx['title'] = _('Password reset') |
|
37 |
return ctx |
|
38 | ||
39 |
def form_valid(self, form): |
|
40 |
form.save() |
|
41 |
# return to next URL |
|
42 |
messages.info(self.request, _('If your email address exists in our ' |
|
43 |
'database, you will receive an email ' |
|
44 |
'containing instructions to reset ' |
|
45 |
'your password')) |
|
46 |
return super(PasswordResetView, self).form_valid(form) |
|
47 | ||
48 |
password_reset = PasswordResetView.as_view() |
|
49 | ||
50 | ||
51 |
class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView): |
|
52 |
'''Validate password reset link, show a set password form and login |
|
53 |
the user. |
|
54 |
''' |
|
55 |
form_class = SetPasswordForm |
|
56 |
title = _('Password Reset') |
|
57 | ||
58 |
def get_template_names(self): |
|
59 |
return [ |
|
60 |
'registration/password_reset_confirm.html', |
|
61 |
'authentic2/password_reset_confirm.html', |
|
62 |
] |
|
63 | ||
64 |
def dispatch(self, request, *args, **kwargs): |
|
65 |
validlink = True |
|
66 |
uidb64 = kwargs['uidb64'] |
|
67 |
self.token = token = kwargs['token'] |
|
68 | ||
69 |
UserModel = get_user_model() |
|
70 |
# checked by URLconf |
|
71 |
assert uidb64 is not None and token is not None |
|
72 |
try: |
|
73 |
uid = urlsafe_base64_decode(uidb64) |
|
74 |
# use authenticate to eventually get an LDAPUser |
|
75 |
self.user = authenticate(user=UserModel._default_manager.get(pk=uid)) |
|
76 |
except (TypeError, ValueError, OverflowError, |
|
77 |
UserModel.DoesNotExist): |
|
78 |
validlink = False |
|
79 |
messages.warning(request, _('User not found')) |
|
80 | ||
81 |
if validlink and not default_token_generator.check_token(self.user, token): |
|
82 |
validlink = False |
|
83 |
messages.warning(request, _('You reset password link is invalid ' |
|
84 |
'or has expired')) |
|
85 |
if not validlink: |
|
86 |
return utils.redirect(request, self.get_success_url()) |
|
87 |
can_reset_password = utils.get_user_flag(user=self.user, |
|
88 |
name='can_reset_password', |
|
89 |
default=self.user.has_usable_password()) |
|
90 |
if not can_reset_password: |
|
91 |
messages.warning(request, _('It\'s not possible to reset your password. Please ' |
|
92 |
'contact an administrator.')) |
|
93 |
return utils.redirect(request, self.get_success_url()) |
|
94 |
return super(PasswordResetConfirmView, self).dispatch(request, *args, |
|
95 |
**kwargs) |
|
96 | ||
97 |
def get_context_data(self, **kwargs): |
|
98 |
ctx = super(PasswordResetConfirmView, self).get_context_data(**kwargs) |
|
99 |
# compatibility with existing templates ! |
|
100 |
ctx['title'] = _('Enter new password') |
|
101 |
ctx['validlink'] = True |
|
102 |
return ctx |
|
103 | ||
104 |
def get_form_kwargs(self): |
|
105 |
kwargs = super(PasswordResetConfirmView, self).get_form_kwargs() |
|
106 |
kwargs['user'] = self.user |
|
107 |
return kwargs |
|
108 | ||
109 |
def form_valid(self, form): |
|
110 |
# Changing password by mail validate the email |
|
111 |
form.user.email_verified = True |
|
112 |
form.save() |
|
113 |
hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token, |
|
114 |
form=form) |
|
115 |
logging.getLogger(__name__).info(u'user %s resetted its password with ' |
|
116 |
'token %r...', self.user, |
|
117 |
self.token[:9]) |
|
118 |
return self.finish() |
|
119 | ||
120 |
def finish(self): |
|
121 |
return utils.simulate_authentication(self.request, self.user, 'email') |
|
122 | ||
123 |
password_reset_confirm = PasswordResetConfirmView.as_view() |
|
124 | ||
125 | ||
126 |
def switch_back(request): |
|
127 |
return utils.switch_back(request) |
src/authentic2/registration_backend/urls.py | ||
---|---|---|
1 |
from django.conf.urls import url |
|
2 | ||
3 |
from django.views.generic.base import TemplateView |
|
4 |
from django.contrib.auth.decorators import login_required |
|
5 | ||
6 |
from .views import RegistrationView, registration_completion, DeleteView, registration_complete |
|
7 | ||
8 |
urlpatterns = [ |
|
9 |
url(r'^activate/(?P<registration_token>[\w: -]+)/$', |
|
10 |
registration_completion, name='registration_activate'), |
|
11 |
url(r'^register/$', |
|
12 |
RegistrationView.as_view(), |
|
13 |
name='registration_register'), |
|
14 |
url(r'^register/complete/$', |
|
15 |
registration_complete, |
|
16 |
name='registration_complete'), |
|
17 |
url(r'^register/closed/$', |
|
18 |
TemplateView.as_view(template_name='registration/registration_closed.html'), |
|
19 |
name='registration_disallowed'), |
|
20 |
url(r'^delete/$', |
|
21 |
login_required(DeleteView.as_view()), |
|
22 |
name='delete_account'), |
|
23 |
] |
src/authentic2/registration_backend/views.py | ||
---|---|---|
1 |
import collections |
|
2 |
import logging |
|
3 |
import random |
|
4 | ||
5 |
from django.conf import settings |
|
6 |
from django.shortcuts import get_object_or_404 |
|
7 |
from django.utils.translation import ugettext as _ |
|
8 |
from django.utils.http import urlquote |
|
9 |
from django.contrib import messages |
|
10 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
|
11 |
from django.core import signing |
|
12 |
from django.views.generic.base import TemplateView |
|
13 |
from django.views.generic.edit import FormView, CreateView |
|
14 |
from django.contrib.auth import get_user_model |
|
15 |
from django.forms import CharField, Form |
|
16 |
from django.core.urlresolvers import reverse_lazy |
|
17 |
from django.http import Http404, HttpResponseBadRequest |
|
18 | ||
19 |
from authentic2.utils import (import_module_or_class, redirect, make_url, get_fields_and_labels, |
|
20 |
simulate_authentication) |
|
21 |
from authentic2.a2_rbac.utils import get_default_ou |
|
22 |
from authentic2 import hooks |
|
23 | ||
24 |
from django_rbac.utils import get_ou_model |
|
25 | ||
26 |
from .. import models, app_settings, compat, cbv, forms, validators, utils, constants |
|
27 |
from .forms import RegistrationCompletionForm, DeleteAccountForm |
|
28 |
from .forms import RegistrationCompletionFormNoPassword |
|
29 |
from authentic2.a2_rbac.models import OrganizationalUnit |
|
30 | ||
31 |
logger = logging.getLogger(__name__) |
|
32 | ||
33 |
User = compat.get_user_model() |
|
34 | ||
35 | ||
36 |
def valid_token(method): |
|
37 |
def f(request, *args, **kwargs): |
|
38 |
try: |
|
39 |
request.token = signing.loads(kwargs['registration_token'].replace(' ', ''), |
|
40 |
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
41 |
except signing.SignatureExpired: |
|
42 |
messages.warning(request, _('Your activation key is expired')) |
|
43 |
return redirect(request, 'registration_register') |
|
44 |
except signing.BadSignature: |
|
45 |
messages.warning(request, _('Activation failed')) |
|
46 |
return redirect(request, 'registration_register') |
|
47 |
return method(request, *args, **kwargs) |
|
48 |
return f |
|
49 | ||
50 | ||
51 |
class BaseRegistrationView(FormView): |
|
52 |
form_class = import_module_or_class(app_settings.A2_REGISTRATION_FORM_CLASS) |
|
53 |
template_name = 'registration/registration_form.html' |
|
54 |
title = _('Registration') |
|
55 | ||
56 |
def dispatch(self, request, *args, **kwargs): |
|
57 |
if not getattr(settings, 'REGISTRATION_OPEN', True): |
|
58 |
raise Http404('Registration is not open.') |
|
59 |
self.token = {} |
|
60 |
self.ou = get_default_ou() |
|
61 |
# load pre-filled values |
|
62 |
if request.GET.get('token'): |
|
63 |
try: |
|
64 |
self.token = signing.loads( |
|
65 |
request.GET.get('token'), |
|
66 |
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
67 |
except (TypeError, ValueError, signing.BadSignature) as e: |
|
68 |
logger.warning(u'registration_view: invalid token: %s', e) |
|
69 |
return HttpResponseBadRequest('invalid token', content_type='text/plain') |
|
70 |
if 'ou' in self.token: |
|
71 |
self.ou = OrganizationalUnit.objects.get(pk=self.token['ou']) |
|
72 |
self.next_url = self.token.pop(REDIRECT_FIELD_NAME, utils.select_next_url(request, None)) |
|
73 |
return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs) |
|
74 | ||
75 |
def form_valid(self, form): |
|
76 |
email = form.cleaned_data.pop('email') |
|
77 |
for field in form.cleaned_data: |
|
78 |
self.token[field] = form.cleaned_data[field] |
|
79 | ||
80 |
# propagate service to the registration completion view |
|
81 |
if constants.SERVICE_FIELD_NAME in self.request.GET: |
|
82 |
self.token[constants.SERVICE_FIELD_NAME] = \ |
|
83 |
self.request.GET[constants.SERVICE_FIELD_NAME] |
|
84 | ||
85 |
self.token.pop(REDIRECT_FIELD_NAME, None) |
|
86 |
self.token.pop('email', None) |
|
87 | ||
88 |
utils.send_registration_mail(self.request, email, next_url=self.next_url, |
|
89 |
ou=self.ou, **self.token) |
|
90 |
self.request.session['registered_email'] = email |
|
91 |
return redirect(self.request, 'registration_complete', params={REDIRECT_FIELD_NAME: self.next_url}) |
|
92 | ||
93 |
def get_context_data(self, **kwargs): |
|
94 |
context = super(BaseRegistrationView, self).get_context_data(**kwargs) |
|
95 |
parameters = {'request': self.request, |
|
96 |
'context': context} |
|
97 |
blocks = [utils.get_authenticator_method(authenticator, 'registration', parameters) |
|
98 |
for authenticator in utils.get_backends('AUTH_FRONTENDS')] |
|
99 |
context['frontends'] = collections.OrderedDict((block['id'], block) |
|
100 |
for block in blocks if block) |
|
101 |
return context |
|
102 | ||
103 | ||
104 |
class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView): |
|
105 |
pass |
|
106 | ||
107 | ||
108 |
class RegistrationCompletionView(CreateView): |
|
109 |
model = get_user_model() |
|
110 |
success_url = 'auth_homepage' |
|
111 | ||
112 |
def get_template_names(self): |
|
113 |
if self.users and not 'create' in self.request.GET: |
|
114 |
return ['registration/registration_completion_choose.html'] |
|
115 |
else: |
|
116 |
return ['registration/registration_completion_form.html'] |
|
117 | ||
118 |
def get_success_url(self): |
|
119 |
try: |
|
120 |
redirect_url, next_field = app_settings.A2_REGISTRATION_REDIRECT |
|
121 |
except Exception: |
|
122 |
redirect_url = app_settings.A2_REGISTRATION_REDIRECT |
|
123 |
next_field = REDIRECT_FIELD_NAME |
|
124 | ||
125 |
if self.token and self.token.get(REDIRECT_FIELD_NAME): |
|
126 |
url = self.token[REDIRECT_FIELD_NAME] |
|
127 |
if redirect_url: |
|
128 |
url = make_url(redirect_url, params={next_field: url}) |
|
129 |
else: |
|
130 |
if redirect_url: |
|
131 |
url = redirect_url |
|
132 |
else: |
|
133 |
url = make_url(self.success_url) |
|
134 |
return url |
|
135 | ||
136 |
def dispatch(self, request, *args, **kwargs): |
|
137 |
self.token = request.token |
|
138 |
self.authentication_method = self.token.get('authentication_method', 'email') |
|
139 |
self.email = request.token['email'] |
|
140 |
if 'ou' in self.token: |
|
141 |
self.ou = OrganizationalUnit.objects.get(pk=self.token['ou']) |
|
142 |
else: |
|
143 |
self.ou = get_default_ou() |
|
144 |
self.users = User.objects.filter(email__iexact=self.email) \ |
|
145 |
.order_by('date_joined') |
|
146 |
if self.ou: |
|
147 |
self.users = self.users.filter(ou=self.ou) |
|
148 |
self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE \ |
|
149 |
or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE |
|
150 |
if self.ou: |
|
151 |
self.email_is_unique |= self.ou.email_is_unique |
|
152 |
self.init_fields_labels_and_help_texts() |
|
153 |
# if registration is done during an SSO add the service to the registration event |
|
154 |
self.service = self.token.get(constants.SERVICE_FIELD_NAME) |
|
155 |
return super(RegistrationCompletionView, self) \ |
|
156 |
.dispatch(request, *args, **kwargs) |
|
157 | ||
158 |
def init_fields_labels_and_help_texts(self): |
|
159 |
attributes = models.Attribute.objects.filter( |
|
160 |
asked_on_registration=True) |
|
161 |
default_fields = attributes.values_list('name', flat=True) |
|
162 |
required_fields = models.Attribute.objects.filter(required=True) \ |
|
163 |
.values_list('name', flat=True) |
|
164 |
fields, labels = get_fields_and_labels( |
|
165 |
app_settings.A2_REGISTRATION_FIELDS, |
|
166 |
default_fields, |
|
167 |
app_settings.A2_REGISTRATION_REQUIRED_FIELDS, |
|
168 |
app_settings.A2_REQUIRED_FIELDS, |
|
169 |
models.Attribute.objects.filter(required=True).values_list('name', flat=True)) |
|
170 |
help_texts = {} |
|
171 |
if app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL: |
|
172 |
labels['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL |
|
173 |
if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT: |
|
174 |
help_texts['username'] = \ |
|
175 |
app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT |
|
176 |
required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + \ |
|
177 |
list(required_fields) |
|
178 |
if 'email' in fields: |
|
179 |
fields.remove('email') |
|
180 |
for field in self.token.get('skip_fields') or []: |
|
181 |
if field in fields: |
|
182 |
fields.remove(field) |
|
183 |
self.fields = fields |
|
184 |
self.labels = labels |
|
185 |
self.required = required |
|
186 |
self.help_texts = help_texts |
|
187 | ||
188 |
def get_form_class(self): |
|
189 |
if not self.token.get('valid_email', True): |
|
190 |
self.fields.append('email') |
|
191 |
self.required.append('email') |
|
192 |
form_class = RegistrationCompletionForm |
|
193 |
if self.token.get('no_password', False): |
|
194 |
form_class = RegistrationCompletionFormNoPassword |
|
195 |
form_class = forms.modelform_factory(self.model, |
|
196 |
form=form_class, |
|
197 |
fields=self.fields, |
|
198 |
labels=self.labels, |
|
199 |
required=self.required, |
|
200 |
help_texts=self.help_texts) |
|
201 |
if 'username' in self.fields and app_settings.A2_REGISTRATION_FORM_USERNAME_REGEX: |
|
202 |
# Keep existing field label and help_text |
|
203 |
old_field = form_class.base_fields['username'] |
|
204 |
field = CharField( |
|
205 |
max_length=256, |
|
206 |
label=old_field.label, |
|
207 |
help_text=old_field.help_text, |
|
208 |
validators=[validators.UsernameValidator()]) |
|
209 |
form_class = type('RegistrationForm', (form_class,), {'username': field}) |
|
210 |
return form_class |
|
211 | ||
212 |
def get_form_kwargs(self, **kwargs): |
|
213 |
'''Initialize mail from token''' |
|
214 |
kwargs = super(RegistrationCompletionView, self).get_form_kwargs(**kwargs) |
|
215 |
if 'ou' in self.token: |
|
216 |
OU = get_ou_model() |
|
217 |
ou = get_object_or_404(OU, id=self.token['ou']) |
|
218 |
else: |
|
219 |
ou = get_default_ou() |
|
220 | ||
221 |
attributes = {'email': self.email, 'ou': ou} |
|
222 |
for key in self.token: |
|
223 |
if key in app_settings.A2_PRE_REGISTRATION_FIELDS: |
|
224 |
attributes[key] = self.token[key] |
|
225 |
logger.debug(u'attributes %s', attributes) |
|
226 | ||
227 |
prefilling_list = utils.accumulate_from_backends(self.request, 'registration_form_prefill') |
|
228 |
logger.debug(u'prefilling_list %s', prefilling_list) |
|
229 |
# Build a single meaningful prefilling with sets of values |
|
230 |
prefilling = {} |
|
231 |
for p in prefilling_list: |
|
232 |
for name, values in p.items(): |
|
233 |
if name in self.fields: |
|
234 |
prefilling.setdefault(name, set()).update(values) |
|
235 |
logger.debug(u'prefilling %s', prefilling) |
|
236 | ||
237 |
for name, values in prefilling.items(): |
|
238 |
attributes[name] = ' '.join(values) |
|
239 |
logger.debug(u'attributes with prefilling %s', attributes) |
|
240 | ||
241 |
if self.token.get('user_id'): |
|
242 |
kwargs['instance'] = User.objects.get(id=self.token.get('user_id')) |
|
243 |
else: |
|
244 |
init_kwargs = {} |
|
245 |
for key in ('email', 'first_name', 'last_name', 'ou'): |
|
246 |
if key in attributes: |
|
247 |
init_kwargs[key] = attributes[key] |
|
248 |
kwargs['instance'] = get_user_model()(**init_kwargs) |
|
249 | ||
250 |
return kwargs |
|
251 | ||
252 |
def get_form(self, form_class=None): |
|
253 |
form = super(RegistrationCompletionView, self).get_form(form_class=form_class) |
|
254 |
hooks.call_hooks('front_modify_form', self, form) |
|
255 |
return form |
|
256 | ||
257 |
def get_context_data(self, **kwargs): |
|
258 |
ctx = super(RegistrationCompletionView, self).get_context_data(**kwargs) |
|
259 |
ctx['token'] = self.token |
|
260 |
ctx['users'] = self.users |
|
261 |
ctx['email'] = self.email |
|
262 |
ctx['email_is_unique'] = self.email_is_unique |
|
263 |
ctx['create'] = 'create' in self.request.GET |
|
264 |
return ctx |
|
265 | ||
266 |
def get(self, request, *args, **kwargs): |
|
267 |
if len(self.users) == 1 and self.email_is_unique: |
|
268 |
# Found one user, EMAIL is unique, log her in |
|
269 |
simulate_authentication(request, self.users[0], |
|
270 |
method=self.authentication_method, |
|
271 |
service_slug=self.service) |
|
272 |
return redirect(request, self.get_success_url()) |
|
273 |
confirm_data = self.token.get('confirm_data', False) |
|
274 | ||
275 |
if confirm_data == 'required': |
|
276 |
fields_to_confirm = self.required |
|
277 |
else: |
|
278 |
fields_to_confirm = self.fields |
|
279 |
if (all(field in self.token for field in fields_to_confirm) |
|
280 |
and (not confirm_data or confirm_data == 'required')): |
|
281 |
# We already have every fields |
|
282 |
form_kwargs = self.get_form_kwargs() |
|
283 |
form_class = self.get_form_class() |
|
284 |
data = self.token |
|
285 |
if 'password' in data: |
|
286 |
data['password1'] = data['password'] |
|
287 |
data['password2'] = data['password'] |
|
288 |
del data['password'] |
|
289 |
form_kwargs['data'] = data |
|
290 |
form = form_class(**form_kwargs) |
|
291 |
if form.is_valid(): |
|
292 |
user = form.save() |
|
293 |
return self.registration_success(request, user, form) |
|
294 |
self.get_form = lambda *args, **kwargs: form |
|
295 |
return super(RegistrationCompletionView, self).get(request, *args, **kwargs) |
|
296 | ||
297 |
def post(self, request, *args, **kwargs): |
|
298 |
if self.users and self.email_is_unique: |
|
299 |
# email is unique, users already exist, creating a new one is forbidden ! |
|
300 |
return redirect(request, request.resolver_match.view_name, args=self.args, |
|
301 |
kwargs=self.kwargs) |
|
302 |
if 'uid' in request.POST: |
|
303 |
uid = request.POST['uid'] |
|
304 |
for user in self.users: |
|
305 |
if str(user.id) == uid: |
|
306 |
simulate_authentication(request, user, |
|
307 |
method=self.authentication_method, |
|
308 |
service_slug=self.service) |
|
309 |
return redirect(request, self.get_success_url()) |
|
310 |
return super(RegistrationCompletionView, self).post(request, *args, **kwargs) |
|
311 | ||
312 |
def form_valid(self, form): |
|
313 | ||
314 |
# remove verified fields from form, this allows an authentication |
|
315 |
# method to provide verified data fields and to present it to the user, |
|
316 |
# while preventing the user to modify them. |
|
317 |
for av in models.AttributeValue.objects.with_owner(form.instance): |
|
318 |
if av.verified and av.attribute.name in form.fields: |
|
319 |
del form.fields[av.attribute.name] |
|
320 | ||
321 |
if ('email' in self.request.POST |
|
322 |
and (not 'email' in self.token or self.request.POST['email'] != self.token['email']) |
|
323 |
and not self.token.get('skip_email_check')): |
|
324 |
# If an email is submitted it must be validated or be the same as in the token |
|
325 |
data = form.cleaned_data |
|
326 |
data['no_password'] = self.token.get('no_password', False) |
|
327 |
utils.send_registration_mail( |
|
328 |
self.request, |
|
329 |
ou=self.ou, |
|
330 |
next_url=self.get_success_url(), |
|
331 |
**data) |
|
332 |
self.request.session['registered_email'] = form.cleaned_data['email'] |
|
333 |
return redirect(self.request, 'registration_complete') |
|
334 |
super(RegistrationCompletionView, self).form_valid(form) |
|
335 |
return self.registration_success(self.request, form.instance, form) |
|
336 | ||
337 |
def registration_success(self, request, user, form): |
|
338 |
hooks.call_hooks('event', name='registration', user=user, form=form, view=self, |
|
339 |
authentication_method=self.authentication_method, |
|
340 |
token=request.token, service=self.service) |
|
341 |
simulate_authentication(request, user, method=self.authentication_method, |
|
342 |
service_slug=self.service) |
|
343 |
messages.info(self.request, _('You have just created an account.')) |
|
344 |
self.send_registration_success_email(user) |
|
345 |
return redirect(request, self.get_success_url()) |
|
346 | ||
347 |
def send_registration_success_email(self, user): |
|
348 |
if not user.email: |
|
349 |
return |
|
350 | ||
351 |
template_names = [ |
|
352 |
'authentic2/registration_success' |
|
353 |
] |
|
354 |
login_url = self.request.build_absolute_uri(settings.LOGIN_URL) |
|
355 |
utils.send_templated_mail(user, template_names=template_names, |
|
356 |
context={ |
|
357 |
'user': user, |
|
358 |
'email': user.email, |
|
359 |
'site': self.request.get_host(), |
|
360 |
'login_url': login_url, |
|
361 |
}, |
|
362 |
request=self.request) |
|
363 | ||
364 | ||
365 |
class DeleteView(FormView): |
|
366 |
template_name = 'authentic2/accounts_delete.html' |
|
367 |
success_url = reverse_lazy('auth_logout') |
|
368 |
title = _('Delete account') |
|
369 | ||
370 |
def dispatch(self, request, *args, **kwargs): |
|
371 |
if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT: |
|
372 |
return redirect(request, '..') |
|
373 |
return super(DeleteView, self).dispatch(request, *args, **kwargs) |
|
374 | ||
375 |
def post(self, request, *args, **kwargs): |
|
376 |
if 'cancel' in request.POST: |
|
377 |
return redirect(request, 'account_management') |
|
378 |
return super(DeleteView, self).post(request, *args, **kwargs) |
|
379 | ||
380 |
def get_form_class(self): |
|
381 |
if self.request.user.has_usable_password(): |
|
382 |
return DeleteAccountForm |
|
383 |
return Form |
|
384 | ||
385 |
def get_form_kwargs(self, **kwargs): |
|
386 |
kwargs = super(DeleteView, self).get_form_kwargs(**kwargs) |
|
387 |
if self.request.user.has_usable_password(): |
|
388 |
kwargs['user'] = self.request.user |
|
389 |
return kwargs |
|
390 | ||
391 |
def form_valid(self, form): |
|
392 |
utils.send_account_deletion_mail(self.request, self.request.user) |
|
393 |
models.DeletedUser.objects.delete_user(self.request.user) |
|
394 |
self.request.user.email += '#%d' % random.randint(1, 10000000) |
|
395 |
self.request.user.email_verified = False |
|
396 |
self.request.user.save(update_fields=['email', 'email_verified']) |
|
397 |
logger.info(u'deletion of account %s requested', self.request.user) |
|
398 |
hooks.call_hooks('event', name='delete-account', user=self.request.user) |
|
399 |
messages.info(self.request, |
|
400 |
_('Your account has been scheduled for deletion. You cannot use it anymore.')) |
|
401 |
return super(DeleteView, self).form_valid(form) |
|
402 | ||
403 |
registration_completion = valid_token(RegistrationCompletionView.as_view()) |
|
404 | ||
405 | ||
406 |
class RegistrationCompleteView(TemplateView): |
|
407 |
template_name = 'registration/registration_complete.html' |
|
408 | ||
409 |
def get_context_data(self, **kwargs): |
|
410 |
kwargs['next_url'] = utils.select_next_url(self.request, settings.LOGIN_REDIRECT_URL) |
|
411 |
return super(RegistrationCompleteView, self).get_context_data( |
|
412 |
account_activation_days=settings.ACCOUNT_ACTIVATION_DAYS, |
|
413 |
**kwargs) |
|
414 | ||
415 | ||
416 |
registration_complete = RegistrationCompleteView.as_view() |
src/authentic2/saml/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.apps import AppConfig |
2 | 18 | |
3 | 19 |
default_app_config = 'authentic2.saml.A2SAMLAppConfig' |
src/authentic2/saml/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 | 19 |
from django.contrib import admin |
... | ... | |
18 | 34 |
SPOptionsIdPPolicy, LibertyFederation, |
19 | 35 |
KeyValue, LibertySession, SAMLAttribute) |
20 | 36 | |
21 |
from authentic2.decorators import to_iter |
|
22 | 37 |
from authentic2.attributes_ng.engine import get_service_attributes |
23 | 38 | |
24 | 39 |
from . import admin_views |
25 | 40 | |
26 | 41 |
logger = logging.getLogger(__name__) |
27 | 42 | |
43 | ||
28 | 44 |
class LibertyServiceProviderInline(admin.StackedInline): |
29 | 45 |
model = LibertyServiceProvider |
30 | 46 | |
47 | ||
31 | 48 |
class TextAndFileWidget(forms.widgets.MultiWidget): |
32 | 49 |
def __init__(self, attrs=None): |
33 |
widgets = (forms.widgets.Textarea(), |
|
34 |
forms.widgets.FileInput(),) |
|
50 |
widgets = (forms.widgets.Textarea(), forms.widgets.FileInput()) |
|
35 | 51 |
super(TextAndFileWidget, self).__init__(widgets, attrs) |
36 | 52 | |
37 | 53 |
def decompress(self, value): |
... | ... | |
58 | 74 | |
59 | 75 | |
60 | 76 |
class LibertyProviderForm(ModelForm): |
61 |
metadata = forms.CharField(required=True, widget=TextAndFileWidget, |
|
62 |
label=_('Metadata')) |
|
63 |
public_key = forms.CharField(required=False, widget=TextAndFileWidget, |
|
64 |
label=_('Public key')) |
|
65 |
ssl_certificate = forms.CharField(required=False, widget=TextAndFileWidget, |
|
66 |
label=_('SSL certificate')) |
|
67 |
ca_cert_chain = forms.CharField(required=False, widget=TextAndFileWidget, |
|
68 |
label=_('Certificate chain')) |
|
77 |
metadata = forms.CharField(required=True, widget=TextAndFileWidget, label=_('Metadata')) |
|
78 |
public_key = forms.CharField(required=False, widget=TextAndFileWidget, label=_('Public key')) |
|
79 |
ssl_certificate = forms.CharField(required=False, widget=TextAndFileWidget, label=_('SSL certificate')) |
|
80 |
ca_cert_chain = forms.CharField(required=False, widget=TextAndFileWidget, label=_('Certificate chain')) |
|
69 | 81 | |
70 | 82 |
class Meta: |
71 | 83 |
model = LibertyProvider |
72 | 84 |
fields = [ |
73 |
'name',
|
|
74 |
'slug',
|
|
75 |
'ou',
|
|
76 |
'entity_id',
|
|
77 |
'entity_id_sha1',
|
|
78 |
'federation_source',
|
|
79 |
'metadata_url',
|
|
80 |
'metadata',
|
|
81 |
'public_key',
|
|
82 |
'ssl_certificate',
|
|
83 |
'ca_cert_chain',
|
|
85 |
'name', |
|
86 |
'slug', |
|
87 |
'ou', |
|
88 |
'entity_id', |
|
89 |
'entity_id_sha1', |
|
90 |
'federation_source', |
|
91 |
'metadata_url', |
|
92 |
'metadata', |
|
93 |
'public_key', |
|
94 |
'ssl_certificate', |
|
95 |
'ca_cert_chain', |
|
84 | 96 |
] |
85 | 97 | |
86 | 98 | |
... | ... | |
93 | 105 |
provider.update_metadata() |
94 | 106 |
except ValidationError as e: |
95 | 107 |
params = { |
96 |
'name': provider,
|
|
97 |
'error_msg': u', '.join(e.messages)
|
|
108 |
'name': provider, |
|
109 |
'error_msg': u', '.join(e.messages) |
|
98 | 110 |
} |
99 |
messages.error(request, _('Updating SAML provider %(name)s failed: ' |
|
100 |
'%(error_msg)s') % params) |
|
111 |
messages.error(request, _('Updating SAML provider %(name)s failed: ' '%(error_msg)s') % params) |
|
101 | 112 |
else: |
102 | 113 |
count += 1 |
103 |
messages.info(request, _('%(count)d on %(total)d SAML providers updated') % { |
|
104 |
'count': count, 'total': total}) |
|
114 |
messages.info(request, _('%(count)d on %(total)d SAML providers updated') % {'count': count, 'total': total}) |
|
105 | 115 | |
106 | 116 | |
107 | 117 |
class SAMLAttributeInlineForm(forms.ModelForm): |
... | ... | |
115 | 125 |
class Meta: |
116 | 126 |
model = SAMLAttribute |
117 | 127 |
fields = [ |
118 |
'name_format',
|
|
119 |
'name',
|
|
120 |
'friendly_name',
|
|
121 |
'attribute_name',
|
|
122 |
'enabled',
|
|
128 |
'name_format', |
|
129 |
'name', |
|
130 |
'friendly_name', |
|
131 |
'attribute_name', |
|
132 |
'enabled', |
|
123 | 133 |
] |
124 | 134 | |
135 | ||
125 | 136 |
class SAMLAttributeInlineAdmin(GenericTabularInline): |
126 | 137 |
model = SAMLAttribute |
127 | 138 |
form = SAMLAttributeInlineForm |
... | ... | |
135 | 146 |
kwargs['form'] = NewForm |
136 | 147 |
return super(SAMLAttributeInlineAdmin, self).get_formset(request, obj=obj, **kwargs) |
137 | 148 | |
149 | ||
138 | 150 |
class LibertyProviderAdmin(admin.ModelAdmin): |
139 | 151 |
form = LibertyProviderForm |
140 | 152 |
list_display = ('name', 'ou', 'slug', 'entity_id') |
141 | 153 |
search_fields = ('name', 'entity_id') |
142 |
readonly_fields = ('entity_id','protocol_conformance','entity_id_sha1','federation_source')
|
|
154 |
readonly_fields = ('entity_id', 'protocol_conformance', 'entity_id_sha1', 'federation_source')
|
|
143 | 155 |
fieldsets = ( |
144 |
(None, {
|
|
145 |
'fields' : ('name', 'slug', 'ou', 'entity_id', 'entity_id_sha1','federation_source')
|
|
146 |
}),
|
|
147 |
(_('Metadata files'), {
|
|
148 |
'fields': ('metadata_url', 'metadata', 'public_key', 'ssl_certificate', 'ca_cert_chain')
|
|
149 |
}),
|
|
156 |
(None, { |
|
157 |
'fields': ('name', 'slug', 'ou', 'entity_id', 'entity_id_sha1', 'federation_source')
|
|
158 |
}), |
|
159 |
(_('Metadata files'), { |
|
160 |
'fields': ('metadata_url', 'metadata', 'public_key', 'ssl_certificate', 'ca_cert_chain') |
|
161 |
}), |
|
150 | 162 |
) |
151 | 163 |
inlines = [ |
152 |
LibertyServiceProviderInline,
|
|
153 |
SAMLAttributeInlineAdmin,
|
|
164 |
LibertyServiceProviderInline, |
|
165 |
SAMLAttributeInlineAdmin, |
|
154 | 166 |
] |
155 |
actions = [ update_metadata ]
|
|
167 |
actions = [update_metadata]
|
|
156 | 168 |
prepopulated_fields = {'slug': ('name',)} |
157 | 169 |
list_filter = ( |
158 |
'service_provider__sp_options_policy',
|
|
159 |
'service_provider__enabled',
|
|
170 |
'service_provider__sp_options_policy', |
|
171 |
'service_provider__enabled', |
|
160 | 172 |
) |
161 | 173 | |
162 | 174 |
def get_urls(self): |
... | ... | |
165 | 177 |
url(r'^add-from-url/$', |
166 | 178 |
self.admin_site.admin_view(admin_views.AddLibertyProviderFromUrlView.as_view(model_admin=self)), |
167 | 179 |
name='saml_libertyprovider_add_from_url'), |
168 |
] + urls
|
|
180 |
] + urls |
|
169 | 181 |
return urls |
170 | 182 | |
183 | ||
171 | 184 |
class LibertyFederationAdmin(admin.ModelAdmin): |
172 | 185 |
search_fields = ('name_id_content', 'user__username') |
173 | 186 |
list_display = ('user', 'creation', 'last_modification', 'name_id_content', 'format', 'sp') |
... | ... | |
179 | 192 |
name_id_format = u'\u2026' + name_id_format[-12:] |
180 | 193 |
return name_id_format |
181 | 194 | |
195 | ||
182 | 196 |
class SPOptionsIdPPolicyAdmin(admin.ModelAdmin): |
183 |
inlines = [ SAMLAttributeInlineAdmin ]
|
|
197 |
inlines = [SAMLAttributeInlineAdmin]
|
|
184 | 198 |
fields = ( |
185 |
'name',
|
|
186 |
'enabled',
|
|
187 |
'prefered_assertion_consumer_binding',
|
|
188 |
'encrypt_nameid',
|
|
189 |
'encrypt_assertion',
|
|
190 |
'authn_request_signed',
|
|
191 |
'idp_initiated_sso',
|
|
192 |
'default_name_id_format',
|
|
193 |
'accepted_name_id_format',
|
|
194 |
'ask_user_consent',
|
|
195 |
'accept_slo',
|
|
196 |
'forward_slo',
|
|
197 |
'needs_iframe_logout',
|
|
198 |
'iframe_logout_timeout',
|
|
199 |
'http_method_for_slo_request',
|
|
199 |
'name', |
|
200 |
'enabled', |
|
201 |
'prefered_assertion_consumer_binding', |
|
202 |
'encrypt_nameid', |
|
203 |
'encrypt_assertion', |
|
204 |
'authn_request_signed', |
|
205 |
'idp_initiated_sso', |
|
206 |
'default_name_id_format', |
|
207 |
'accepted_name_id_format', |
|
208 |
'ask_user_consent', |
|
209 |
'accept_slo', |
|
210 |
'forward_slo', |
|
211 |
'needs_iframe_logout', |
|
212 |
'iframe_logout_timeout', |
|
213 |
'http_method_for_slo_request', |
|
200 | 214 |
) |
201 | 215 | |
202 | 216 |
admin.site.register(SPOptionsIdPPolicy, SPOptionsIdPPolicyAdmin) |
src/authentic2/saml/admin_views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.core.urlresolvers import reverse |
2 | 18 |
from django.views.generic import FormView |
3 | 19 | |
4 | 20 |
from .forms import AddLibertyProviderFromUrlForm |
5 | 21 | |
22 | ||
6 | 23 |
class AdminAddFormViewMixin(object): |
7 | 24 |
model_admin = None |
8 | 25 | |
... | ... | |
11 | 28 |
ctx.update({ |
12 | 29 |
'app_label': self.model_admin.model._meta.app_label, |
13 | 30 |
'has_change_permission': self.model_admin.has_change_permission(self.request), |
14 |
'opts': self.model_admin.model._meta }) |
|
31 |
'opts': self.model_admin.model._meta |
|
32 |
}) |
|
15 | 33 |
return ctx |
16 | 34 | |
35 | ||
17 | 36 |
class AddLibertyProviderFromUrlView(AdminAddFormViewMixin, FormView): |
18 | 37 |
form_class = AddLibertyProviderFromUrlForm |
19 | 38 |
template_name = 'admin/saml/libertyprovider/add_from_url.html' |
... | ... | |
27 | 46 | |
28 | 47 |
def form_valid(self, form): |
29 | 48 |
form.save() |
30 |
self.success_url = reverse( |
|
31 |
'admin:saml_libertyprovider_change', |
|
32 |
args=(form.instance.id,)) |
|
49 |
self.success_url = reverse('admin:saml_libertyprovider_change', args=(form.instance.id,)) |
|
33 | 50 |
return super(AddLibertyProviderFromUrlView, self).form_valid(form) |
src/authentic2/saml/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import sys |
2 | 18 | |
3 | 19 |
from django.conf import settings |
20 |
from django.core.exceptions import ImproperlyConfigured |
|
4 | 21 |
from django.utils.translation import ugettext_lazy as _ |
5 | 22 | |
23 | ||
6 | 24 |
class AppSettings(object): |
7 | 25 |
__PREFIX = 'SAML_' |
8 | 26 |
__NAMES = ('ALLOWED_FEDERATION_MODE', 'DEFAULT_FEDERATION_MODE') |
... | ... | |
16 | 34 | |
17 | 35 |
@classmethod |
18 | 36 |
def get_choices(cls, app_settings): |
19 |
l = []
|
|
37 |
choices = []
|
|
20 | 38 |
for choice in cls.choices: |
21 | 39 |
if choice[0] in app_settings.ALLOWED_FEDERATION_MODE: |
22 |
l.append(choice)
|
|
23 |
return l
|
|
40 |
choices.append(choice)
|
|
41 |
return choices
|
|
24 | 42 | |
25 | 43 |
@classmethod |
26 | 44 |
def get_default(cls, app_settings): |
27 | 45 |
return app_settings.DEFAULT_FEDERATION_MODE |
28 | 46 | |
29 | 47 |
__DEFAULTS = { |
30 |
'ALLOWED_FEDERATION_MODE': (FEDERATION_MODE.EXPLICIT, |
|
31 |
FEDERATION_MODE.IMPLICIT), |
|
32 |
'DEFAULT_FEDERATION_MODE': FEDERATION_MODE.EXPLICIT, |
|
48 |
'ALLOWED_FEDERATION_MODE': (FEDERATION_MODE.EXPLICIT, FEDERATION_MODE.IMPLICIT), |
|
49 |
'DEFAULT_FEDERATION_MODE': FEDERATION_MODE.EXPLICIT, |
|
33 | 50 |
} |
34 | 51 | |
35 | ||
36 | 52 |
def __settings(self, name): |
37 | 53 |
full_name = self.__PREFIX + name |
38 | 54 |
if name not in self.__NAMES: |
39 |
raise AttributeError('unknown settings '+full_name)
|
|
55 |
raise AttributeError('unknown settings ' + full_name)
|
|
40 | 56 |
try: |
41 | 57 |
if name in self.__DEFAULTS: |
42 | 58 |
return getattr(settings, full_name, self.__DEFAULTS[name]) |
43 | 59 |
else: |
44 | 60 |
return getattr(settings, full_name) |
45 | 61 |
except AttributeError: |
46 |
raise ImproperlyConfigured('missing settings '+full_name)
|
|
62 |
raise ImproperlyConfigured('missing settings ' + full_name)
|
|
47 | 63 | |
48 | 64 |
def __getattr__(self, name): |
49 | 65 |
return self.__settings(name) |
src/authentic2/saml/common.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import os.path |
2 | 18 |
import logging |
3 | 19 |
import re |
... | ... | |
24 | 40 |
from .. import nonce |
25 | 41 | |
26 | 42 |
AUTHENTIC_STATUS_CODE_NS = "http://authentic.entrouvert.org/status_code/" |
27 |
AUTHENTIC_SAME_ID_SENTINEL = \ |
|
28 |
'urn:authentic.entrouvert.org:same-as-provider-entity-id' |
|
29 |
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER = AUTHENTIC_STATUS_CODE_NS + \ |
|
30 |
"UnknownProvider" |
|
31 |
AUTHENTIC_STATUS_CODE_MISSING_NAMEID = AUTHENTIC_STATUS_CODE_NS + \ |
|
32 |
"MissingNameID" |
|
33 |
AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX = AUTHENTIC_STATUS_CODE_NS + \ |
|
34 |
"MissingSessionIndex" |
|
35 |
AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION = AUTHENTIC_STATUS_CODE_NS + \ |
|
36 |
"UnknownSession" |
|
37 |
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION = AUTHENTIC_STATUS_CODE_NS + \ |
|
38 |
"MissingDestination" |
|
39 |
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR = AUTHENTIC_STATUS_CODE_NS + \ |
|
40 |
"InternalServerError" |
|
41 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED = AUTHENTIC_STATUS_CODE_NS + \ |
|
42 |
"Unauthorized" |
|
43 |
AUTHENTIC_SAME_ID_SENTINEL = 'urn:authentic.entrouvert.org:same-as-provider-entity-id' |
|
44 |
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER = AUTHENTIC_STATUS_CODE_NS + "UnknownProvider" |
|
45 |
AUTHENTIC_STATUS_CODE_MISSING_NAMEID = AUTHENTIC_STATUS_CODE_NS + "MissingNameID" |
|
46 |
AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX = AUTHENTIC_STATUS_CODE_NS + "MissingSessionIndex" |
|
47 |
AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION = AUTHENTIC_STATUS_CODE_NS + "UnknownSession" |
|
48 |
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION = AUTHENTIC_STATUS_CODE_NS + "MissingDestination" |
|
49 |
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR = AUTHENTIC_STATUS_CODE_NS + "InternalServerError" |
|
50 |
AUTHENTIC_STATUS_CODE_UNAUTHORIZED = AUTHENTIC_STATUS_CODE_NS + "Unauthorized" |
|
43 | 51 | |
44 | 52 |
logger = logging.getLogger(__name__) |
45 | 53 | |
... | ... | |
225 | 233 |
delta = datetime.timedelta(seconds=NONCE_TIMEOUT) |
226 | 234 |
if not (now - delta <= issue_instant < now + delta): |
227 | 235 |
logger.warning('IssueInstant %s not in the interval [%s, %s[', |
228 |
issue_instant, now-delta, now+delta)
|
|
236 |
issue_instant, now - delta, now+delta)
|
|
229 | 237 |
return False |
230 | 238 |
except ValueError: |
231 | 239 |
logger.error('Unable to parse an IssueInstant: %r', issue_instant) |
... | ... | |
235 | 243 |
if _id is None: |
236 | 244 |
logger.warning('missing ID') |
237 | 245 |
return False |
238 |
if not nonce.accept_nonce(_id, 'SAML', 2*NONCE_TIMEOUT):
|
|
246 |
if not nonce.accept_nonce(_id, 'SAML', 2 * NONCE_TIMEOUT):
|
|
239 | 247 |
logger.warning("ID '%r' already used, request/response/assertion " |
240 | 248 |
"refused", _id) |
241 | 249 |
return False |
... | ... | |
266 | 274 | |
267 | 275 | |
268 | 276 |
def federations_to_identity_dump(self_entity_id, federations): |
269 |
l = [START_IDENTITY_DUMP] |
|
277 |
l = [START_IDENTITY_DUMP] # noqa: E741
|
|
270 | 278 |
for federation in federations: |
271 | 279 |
name_id_qualifier = federation.name_id_qualifier |
272 | 280 |
name_id_sp_name_qualifier = federation.name_id_sp_name_qualifier |
... | ... | |
325 | 333 |
return None |
326 | 334 |
logger.debug('loaded %d bytes', len(metadata)) |
327 | 335 |
try: |
328 |
metadata = six.text_type(metadata, 'utf8') |
|
329 |
except: |
|
330 |
logging.error('SAML metadata autoload: retrieved metadata for entity ' |
|
331 |
'id %s is not UTF-8', provider_id) |
|
336 |
metadata = six.text_type(metadata, 'utf-8') |
|
337 |
except UnicodeDecodeError: |
|
338 |
logging.error('SAML metadata autoload: retrieved metadata for entity id %s is not UTF-8', provider_id) |
|
332 | 339 |
return None |
333 | 340 |
p = LibertyProvider(metadata=metadata) |
334 | 341 |
try: |
... | ... | |
337 | 344 |
logging.error('SAML metadata autoload: retrieved metadata for entity ' |
338 | 345 |
'id %s are invalid, %s', provider_id, e.args) |
339 | 346 |
return None |
340 |
except: |
|
347 |
except Exception:
|
|
341 | 348 |
logging.exception('SAML metadata autoload: retrieved metadata ' |
342 | 349 |
'validation raised an unknown exception') |
343 | 350 |
return None |
... | ... | |
419 | 426 |
kwargs = models.nameid2kwargs(name_id) |
420 | 427 |
try: |
421 | 428 |
return LibertyFederation.objects.get(**kwargs) |
422 |
except: |
|
429 |
except LibertyFederation.DoesNotExist:
|
|
423 | 430 |
return None |
424 | 431 | |
425 | 432 | |
... | ... | |
431 | 438 |
.identity_provider |
432 | 439 |
try: |
433 | 440 |
return LibertyFederation.objects.get(user__isnull=False, **kwargs) |
434 |
except: |
|
441 |
except LibertyFederation.DoesNotExist:
|
|
435 | 442 |
return None |
436 | 443 | |
437 | 444 |
src/authentic2/saml/fields.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
try: |
2 | 18 |
import cPickle as pickle |
3 | 19 |
except ImportError: |
4 | 20 |
import pickle |
5 | 21 |
import six |
6 | 22 | |
7 |
import django |
|
8 | 23 |
from django import forms |
9 | 24 |
from django.db import models |
10 | 25 |
from django.db.models.lookups import Exact, In |
11 | 26 |
from django.core.exceptions import ValidationError |
12 | 27 |
from django.utils.text import capfirst |
13 |
from django.contrib.humanize.templatetags.humanize \ |
|
14 |
import apnumber |
|
28 |
from django.contrib.humanize.templatetags.humanize import apnumber |
|
15 | 29 |
from django.template.defaultfilters import pluralize |
16 | 30 | |
17 | 31 |
# This is a copy of http://djangosnippets.org/snippets/513/ |
... | ... | |
26 | 40 |
# |
27 | 41 |
# Initial author: Oliver Beattie |
28 | 42 | |
43 | ||
29 | 44 |
class PickledObject(six.binary_type): |
30 | 45 |
"""A subclass of string so it can be told whether a string is |
31 | 46 |
a pickled object or not (if the object is an instance of this class |
... | ... | |
56 | 71 |
else: |
57 | 72 |
try: |
58 | 73 |
return pickle.loads(six.binary_type(value)) |
59 |
except: |
|
74 |
except Exception:
|
|
60 | 75 |
# If an error was raised, just return the plain value |
61 | 76 |
return value |
62 | 77 | |
... | ... | |
111 | 126 |
# |
112 | 127 |
# Initial author: Daniel Roseman |
113 | 128 | |
129 | ||
114 | 130 |
class MultiSelectFormField(forms.MultipleChoiceField): |
115 | 131 |
widget = forms.CheckboxSelectMultiple |
116 | 132 | |
... | ... | |
122 | 138 |
if not value and self.required: |
123 | 139 |
raise forms.ValidationError(self.error_messages['required']) |
124 | 140 |
if value and self.max_choices and len(value) > self.max_choices: |
125 |
raise forms.ValidationError('You must select a maximum of %s choice%s.' |
|
126 |
% (apnumber(self.max_choices), pluralize(self.max_choices)))
|
|
141 |
raise forms.ValidationError('You must select a maximum of %s choice%s.' % (
|
|
142 |
apnumber(self.max_choices), pluralize(self.max_choices))) |
|
127 | 143 |
return value |
128 | 144 | |
129 |
class MultiSelectField(models.Field): |
|
130 | 145 | |
146 |
class MultiSelectField(models.Field): |
|
131 | 147 |
def get_internal_type(self): |
132 | 148 |
return "CharField" |
133 | 149 | |
... | ... | |
139 | 155 | |
140 | 156 |
def formfield(self, **kwargs): |
141 | 157 |
# don't call super, as that overrides default widget if it has choices |
142 |
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), |
|
143 |
'help_text': self.help_text, 'choices':self.choices} |
|
158 |
defaults = { |
|
159 |
'required': not self.blank, |
|
160 |
'label': capfirst(self.verbose_name), |
|
161 |
'help_text': self.help_text, |
|
162 |
'choices': self.choices, |
|
163 |
} |
|
144 | 164 |
if self.has_default(): |
145 | 165 |
defaults['initial'] = self.get_default() |
146 | 166 |
defaults.update(kwargs) |
... | ... | |
155 | 175 |
def validate(self, value, model_instance): |
156 | 176 |
out = set() |
157 | 177 |
if self.choices: |
158 |
out |= set([option_key for option_key,_ in self.choices]) |
|
159 |
out = set(value)-out
|
|
178 |
out |= set([option_key for option_key, _ in self.choices])
|
|
179 |
out = set(value) - out
|
|
160 | 180 |
if out: |
161 | 181 |
raise ValidationError(self.error_messages['invalid_choice'] % ','.join(list(out))) |
162 | 182 |
if not value and not self.blank: |
... | ... | |
175 | 195 |
def contribute_to_class(self, cls, name): |
176 | 196 |
super(MultiSelectField, self).contribute_to_class(cls, name) |
177 | 197 |
if self.choices: |
178 |
func = lambda self, fieldname = name, choicedict = dict(self.choices):",".join([choicedict.get(value,value) for value in getattr(self,fieldname)]) |
|
198 |
def func(self, fieldname=name, choicedict=dict(self.choices)): |
|
199 |
return ",".join([choicedict.get(value, value) for value in getattr(self, fieldname)]) |
|
179 | 200 |
setattr(cls, 'get_%s_display' % self.name, func) |
src/authentic2/saml/forms.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import xml.etree.ElementTree as ET |
2 | 18 | |
3 | 19 |
import requests |
... | ... | |
59 | 75 |
return cleaned_data |
60 | 76 | |
61 | 77 |
def save(self): |
62 |
if not self.instance is None:
|
|
78 |
if self.instance is not None:
|
|
63 | 79 |
self.instance.save() |
64 | 80 |
for child in self.childs: |
65 | 81 |
child.liberty_provider = self.instance |
src/authentic2/saml/lasso_helper.py | ||
---|---|---|
1 |
import xml.etree.ElementTree as etree |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
2 | 16 | |
17 |
import xml.etree.ElementTree as etree |
|
3 | 18 | |
4 | 19 | |
5 | 20 |
LASSO_NS = 'http://www.entrouvert.org/namespaces/lasso/0.0' |
6 | 21 |
SAML_ASSERTION_NS = 'urn:oasis:names:tc:SAML:2.0:assertion' |
7 | 22 | |
23 | ||
8 | 24 |
def lasso_elt(name): |
9 | 25 |
return '{{{0}}}{1}'.format(LASSO_NS, name) |
10 | 26 | |
27 | ||
11 | 28 |
def samla_elt(name): |
12 | 29 |
return '{{{0}}}{1}'.format(SAML_ASSERTION_NS, name) |
13 | 30 | |
31 | ||
14 | 32 |
SESSION_ELT = lasso_elt('Session') |
15 | 33 |
NID_AND_SESSION_INDEX = lasso_elt('NidAndSessionIndex') |
16 | 34 |
VERSION_AT = 'Version' |
... | ... | |
23 | 41 |
NAME_QUALIFIER_AT = 'NameQualifier' |
24 | 42 |
SP_NAME_QUALIFIER_AT = 'SPNameQualifier' |
25 | 43 | |
44 | ||
26 | 45 |
def build_name_id(name_id, treebuilder=None): |
27 | 46 |
if treebuilder is None: |
28 | 47 |
tb = etree.TreeBuilder() |
29 | 48 |
else: |
30 | 49 |
tb = treebuilder |
31 |
attrs = { FORMAT_AT: name_id['name_id_format'] }
|
|
50 |
attrs = {FORMAT_AT: name_id['name_id_format']}
|
|
32 | 51 |
if 'name_id_qualifier' in name_id: |
33 | 52 |
attrs[NAME_QUALIFIER_AT] = name_id['name_id_qualifier'] |
34 | 53 |
if 'name_id_sp_name_qualifier' in name_id: |
... | ... | |
39 | 58 |
if treebuilder is None: |
40 | 59 |
return tb.close() |
41 | 60 | |
61 | ||
42 | 62 |
def buid_session_dump(sessions): |
43 | 63 |
tb = etree.TreeBuilder() |
44 | 64 |
tb.start(SESSION_ELT, {VERSION_AT: '2'}) |
src/authentic2/saml/management/commands/mapping.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
''' |
2 | 18 |
Authentic 2 - Versatile Identity Server |
3 | 19 |
src/authentic2/saml/management/commands/sync-metadata.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import sys |
... | ... | |
7 | 23 |
import warnings |
8 | 24 | |
9 | 25 |
from django.core.management.base import BaseCommand, CommandError |
26 |
from django.db.transaction import atomic |
|
10 | 27 |
from django.template.defaultfilters import slugify |
11 | 28 |
from django.utils import six |
12 | 29 |
from django.utils.translation import gettext as _ |
13 | 30 |
from django.contrib.contenttypes.models import ContentType |
14 | 31 | |
15 |
from authentic2.compat import commit_on_success |
|
16 | 32 |
from authentic2.compat_lasso import lasso |
17 | 33 |
from authentic2.saml.shibboleth.afp_parser import parse_attribute_filters_file |
18 | 34 |
from authentic2.saml.models import (LibertyProvider, SAMLAttribute, LibertyServiceProvider, |
... | ... | |
93 | 109 | |
94 | 110 |
def text_child(tree, tag, default=''): |
95 | 111 |
elt = tree.find(tag) |
96 |
return elt.text if not elt is None else default
|
|
112 |
return elt.text if elt is not None else default
|
|
97 | 113 | |
98 | 114 | |
99 | 115 |
def load_acs(tree, provider, pks, verbosity): |
... | ... | |
129 | 145 |
attribute, created = SAMLAttribute.objects.get_or_create( |
130 | 146 |
defaults=defaults, **kwargs) |
131 | 147 |
if created and verbosity > 1: |
132 |
print(_('Created new attribute %(name)s for %(provider)s') % \ |
|
133 |
{'name': oid, 'provider': provider}) |
|
148 |
print(_('Created new attribute %(name)s for %(provider)s') % { |
|
149 |
'name': oid, |
|
150 |
'provider': provider |
|
151 |
}) |
|
134 | 152 |
pks.append(attribute.pk) |
135 | 153 |
except SAMLAttribute.MultipleObjectsReturned: |
136 | 154 |
pks.extend(SAMLAttribute.objects.filter(**kwargs).values_list('pk', flat=True)) |
... | ... | |
201 | 219 |
kwargs, defaults = build_saml_attribute_kwargs(provider, name) |
202 | 220 |
if not kwargs: |
203 | 221 |
if verbosity > 1: |
204 |
print(_('Unable to find an LDAP definition for attribute %(name)s on %(provider)s') % \ |
|
205 |
{'name': name, 'provider': provider}, file=sys.stderr) |
|
222 |
print(_('Unable to find an LDAP definition for attribute %(name)s on %(provider)s') % { |
|
223 |
'name': name, |
|
224 |
'provider': provider |
|
225 |
}, file=sys.stderr) |
|
206 | 226 |
continue |
207 | 227 |
# create object with default attribute mapping to the same name |
208 | 228 |
# as the attribute if no SAMLAttribute model already exists, |
... | ... | |
211 | 231 |
attribute, created = SAMLAttribute.objects.get_or_create( |
212 | 232 |
defaults=defaults, **kwargs) |
213 | 233 |
if created and verbosity > 1: |
214 |
print(_('Created new attribute %(name)s for %(provider)s') |
|
215 |
% {'name': name, 'provider': provider}) |
|
234 |
print(_('Created new attribute %(name)s for %(provider)s') % { |
|
235 |
'name': name, |
|
236 |
'provider': provider |
|
237 |
}) |
|
216 | 238 |
pks.append(attribute.pk) |
217 | 239 |
except SAMLAttribute.MultipleObjectsReturned: |
218 | 240 |
pks.extend(SAMLAttribute.objects.filter( |
... | ... | |
290 | 312 |
help='When creating a new provider, make it disabled by default.' |
291 | 313 |
) |
292 | 314 | |
293 |
@commit_on_success
|
|
315 |
@atomic
|
|
294 | 316 |
def handle(self, *args, **options): |
295 | 317 |
verbosity = int(options['verbosity']) |
296 | 318 |
source = options['source'] |
... | ... | |
299 | 321 |
try: |
300 | 322 |
if source is not None: |
301 | 323 |
source.decode('ascii') |
302 |
except: |
|
324 |
except UnicodeDecodeError:
|
|
303 | 325 |
raise CommandError('--source MUST be an ASCII string value') |
304 | 326 |
if metadata_file_path.startswith('http://') or metadata_file_path.startswith('https://'): |
305 | 327 |
response = requests.get(metadata_file_path) |
... | ... | |
308 | 330 |
metadata_file = six.BytesIO(response.content) |
309 | 331 |
else: |
310 | 332 |
try: |
311 |
metadata_file = file(metadata_file_path)
|
|
312 |
except: |
|
333 |
metadata_file = open(metadata_file_path)
|
|
334 |
except IOError:
|
|
313 | 335 |
raise CommandError('Unable to open file %s' % metadata_file_path) |
314 | 336 | |
315 | 337 |
try: |
... | ... | |
335 | 357 |
if verbosity > 1: |
336 | 358 |
print('Service providers are set with the following SAML2 \ |
337 | 359 |
options policy: %s' % sp_policy) |
338 |
except: |
|
360 |
except SPOptionsIdPPolicy.DoesNoextExist:
|
|
339 | 361 |
if verbosity > 0: |
340 | 362 |
print(_('SAML2 service provider options ' |
341 | 363 |
'policy with name %s not found') % sp_policy_name, |
... | ... | |
356 | 378 |
sp_policy=sp_policy, |
357 | 379 |
afp=afp) |
358 | 380 |
loaded.append(entity_descriptor.get(ENTITY_ID)) |
359 |
except Exception as e:
|
|
381 |
except Exception: |
|
360 | 382 |
if not options['ignore-errors']: |
361 | 383 |
raise |
362 | 384 |
if verbosity > 0: |
363 |
print((_('Failed to load entity descriptor for %s') |
|
364 |
% entity_descriptor.get(ENTITY_ID)), |
|
365 |
file=sys.stderr) |
|
385 |
print(_('Failed to load entity descriptor for %s') % entity_descriptor.get(ENTITY_ID), |
|
386 |
file=sys.stderr) |
|
366 | 387 |
raise CommandError() |
367 | 388 |
if options['source']: |
368 | 389 |
if options['delete']: |
src/authentic2/saml/managers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import base64 |
2 | 18 |
import binascii |
3 | 19 |
import datetime |
... | ... | |
17 | 33 | |
18 | 34 |
federation_delete = Signal() |
19 | 35 | |
36 | ||
20 | 37 |
class SessionLinkedQuerySet(QuerySet): |
21 | 38 |
def cleanup(self): |
22 | 39 |
engine = import_module(settings.SESSION_ENGINE) |
... | ... | |
28 | 45 | |
29 | 46 |
SessionLinkedManager = models.Manager.from_queryset(SessionLinkedQuerySet) |
30 | 47 | |
48 | ||
31 | 49 |
class LibertyFederationManager(models.Manager): |
32 | 50 |
def cleanup(self): |
33 | 51 |
for federation in self.filter(user__isnull=True): |
... | ... | |
49 | 67 |
class LibertyArtifactManager(models.Manager): |
50 | 68 |
def cleanup(self): |
51 | 69 |
expire = getattr(settings, 'SAML2_ARTIFACT_EXPIRATION', 600) |
52 |
before = now()-datetime.timedelta(seconds=expire)
|
|
70 |
before = now() - datetime.timedelta(seconds=expire)
|
|
53 | 71 |
self.filter(creation__lt=before).delete() |
54 | 72 | |
55 | 73 | |
... | ... | |
59 | 77 |
25-th byte of the given artifact''' |
60 | 78 |
try: |
61 | 79 |
artifact = base64.b64decode(artifact) |
62 |
except: |
|
80 |
except (TypeError, ValueError):
|
|
63 | 81 |
raise ValueError('artifact %r is not a base64 encoded value') |
64 | 82 |
entity_id_sha1 = artifact[4:24] |
65 | 83 |
entity_id_sha1 = binascii.hexlify(entity_id_sha1) |
... | ... | |
79 | 97 | |
80 | 98 |
LibertyProviderManager = models.Manager.from_queryset(LibertyProviderQueryset) |
81 | 99 | |
100 | ||
82 | 101 |
class LibertySessionQuerySet(SessionLinkedQuerySet): |
83 | 102 |
def to_session_dump(self): |
84 |
sessions = self.values('provider_id', |
|
85 |
'session_index', |
|
86 |
'name_id_qualifier', |
|
87 |
'name_id_format', |
|
88 |
'name_id_content', |
|
89 |
'name_id_sp_name_qualifier') |
|
103 |
sessions = self.values( |
|
104 |
'provider_id', |
|
105 |
'session_index', |
|
106 |
'name_id_qualifier', |
|
107 |
'name_id_format', |
|
108 |
'name_id_content', |
|
109 |
'name_id_sp_name_qualifier') |
|
90 | 110 |
return lasso_helper.build_session_dump(sessions) |
91 | 111 | |
92 | 112 |
LibertySessionManager = models.Manager.from_queryset(LibertySessionQuerySet) |
93 | 113 | |
114 | ||
94 | 115 |
class GetByLibertyProviderManager(models.Manager): |
95 | 116 |
def get_by_natural_key(self, slug): |
96 | 117 |
from .models import LibertyProvider |
... | ... | |
103 | 124 |
raise self.model.DoesNotExist |
104 | 125 |
return self.create(liberty_provider=liberty_provider) |
105 | 126 | |
127 | ||
106 | 128 |
class SAMLAttributeManager(GenericManager): |
107 | 129 |
def get_by_natural_key(self, ct_nk, provider_nk, name_format, name, friendly_name, attribute_name): |
108 | 130 |
from .models import SAMLAttribute |
src/authentic2/saml/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import xml.etree.ElementTree as etree |
2 | 18 |
import hashlib |
3 |
import numbers |
|
4 |
import datetime |
|
5 | 19 | |
6 | 20 |
import requests |
7 | 21 |
from authentic2.compat_lasso import lasso |
... | ... | |
30 | 44 |
from .. import managers as a2_managers |
31 | 45 |
from ..models import Service |
32 | 46 | |
47 | ||
33 | 48 |
def metadata_validator(meta): |
34 |
provider=lasso.Provider.newFromBuffer(lasso.PROVIDER_ROLE_ANY, meta.encode('utf8'))
|
|
49 |
provider = lasso.Provider.newFromBuffer(lasso.PROVIDER_ROLE_ANY, meta.encode('utf8'))
|
|
35 | 50 |
if not provider: |
36 | 51 |
raise ValidationError(_('Invalid metadata file')) |
37 | 52 |
XML_NS = 'http://www.w3.org/XML/1998/namespace' |
38 | 53 | |
54 | ||
39 | 55 |
def get_lang(etree): |
40 | 56 |
return etree.get('{%s}lang' % XML_NS) |
41 | 57 | |
58 | ||
42 | 59 |
def ls_find(ls, value): |
43 | 60 |
try: |
44 | 61 |
return ls.index(value) |
45 | 62 |
except ValueError: |
46 | 63 |
return -1 |
47 | 64 | |
48 |
def get_prefered_content(etrees, languages = [None, 'en']): |
|
65 | ||
66 |
def get_prefered_content(etrees, languages=[None, 'en']): |
|
49 | 67 |
'''Sort XML nodes by their xml:lang attribute using languages as the |
50 | 68 |
ascending partial order of language identifiers |
51 | 69 | |
... | ... | |
65 | 83 |
best_score = ls_find(languages, get_lang(tree)) |
66 | 84 |
return best.text |
67 | 85 | |
86 | ||
68 | 87 |
def organization_name(provider): |
69 | 88 |
'''Extract an organization name from a SAMLv2 metadata organization XML |
70 | 89 |
fragment. |
... | ... | |
72 | 91 |
try: |
73 | 92 |
organization_xml = provider.organization |
74 | 93 |
organization = etree.XML(organization_xml) |
75 |
o_display_name = organization.findall('{%s}OrganizationDisplayName' % |
|
76 |
lasso.SAML2_METADATA_HREF) |
|
94 |
o_display_name = organization.findall('{%s}OrganizationDisplayName' % lasso.SAML2_METADATA_HREF) |
|
77 | 95 |
if o_display_name: |
78 | 96 |
return get_prefered_content(o_display_name) |
79 |
o_name = organization.findall('{%s}OrganizationName' % |
|
80 |
lasso.SAML2_METADATA_HREF) |
|
97 |
o_name = organization.findall('{%s}OrganizationName' % lasso.SAML2_METADATA_HREF) |
|
81 | 98 |
if o_name: |
82 | 99 |
return get_prefered_content(o_name) |
83 |
except: |
|
100 |
except Exception:
|
|
84 | 101 |
return provider.providerId |
85 | 102 |
else: |
86 | 103 |
return provider.providerId |
87 | 104 | |
88 | 105 |
# TODO: Remove this in LibertyServiceProvider |
89 | 106 |
ASSERTION_CONSUMER_PROFILES = ( |
90 |
('meta', _('Use the default from the metadata file')),
|
|
91 |
('art', _('Artifact binding')),
|
|
92 |
('post', _('POST binding')))
|
|
107 |
('meta', _('Use the default from the metadata file')), |
|
108 |
('art', _('Artifact binding')), |
|
109 |
('post', _('POST binding'))) |
|
93 | 110 | |
94 | 111 |
DEFAULT_NAME_ID_FORMAT = 'none' |
95 | 112 | |
96 | 113 |
# Supported name id formats |
97 | 114 |
NAME_ID_FORMATS = { |
98 |
'none': { 'caption': _('None'), |
|
99 |
'samlv2': None,}, |
|
100 |
'persistent': { 'caption': _('Persistent'), |
|
101 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,}, |
|
102 |
'transient': { 'caption': _("Transient"), |
|
103 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT,}, |
|
104 |
'email': { 'caption': _("Email"), |
|
105 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL,}, |
|
106 |
'username': { 'caption': _("Username (use with Google Apps)"), |
|
107 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,}, |
|
108 |
'uuid': { 'caption': _("UUID"), |
|
109 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,}, |
|
110 |
'edupersontargetedid': { 'caption': _("Use eduPersonTargetedID attribute"), |
|
111 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,} |
|
115 |
'none': { |
|
116 |
'caption': _('None'), |
|
117 |
'samlv2': None, |
|
118 |
}, |
|
119 |
'persistent': { |
|
120 |
'caption': _('Persistent'), |
|
121 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, |
|
122 |
}, |
|
123 |
'transient': { |
|
124 |
'caption': _("Transient"), |
|
125 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT, |
|
126 |
}, |
|
127 |
'email': { |
|
128 |
'caption': _("Email"), |
|
129 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL, |
|
130 |
}, |
|
131 |
'username': { |
|
132 |
'caption': _("Username (use with Google Apps)"), |
|
133 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, |
|
134 |
}, |
|
135 |
'uuid': { |
|
136 |
'caption': _("UUID"), |
|
137 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, |
|
138 |
}, |
|
139 |
'edupersontargetedid': { |
|
140 |
'caption': _("Use eduPersonTargetedID attribute"), |
|
141 |
'samlv2': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, |
|
142 |
} |
|
112 | 143 |
} |
113 | 144 | |
114 |
NAME_ID_FORMATS_CHOICES = \ |
|
115 |
tuple([(x, y['caption']) for x, y in NAME_ID_FORMATS.items()]) |
|
145 |
NAME_ID_FORMATS_CHOICES = tuple([(x, y['caption']) for x, y in NAME_ID_FORMATS.items()]) |
|
146 | ||
147 |
ACCEPTED_NAME_ID_FORMAT_LENGTH = sum([len(x) for x, y in NAME_ID_FORMATS.items()]) + len(NAME_ID_FORMATS) - 1 |
|
116 | 148 | |
117 |
ACCEPTED_NAME_ID_FORMAT_LENGTH = \ |
|
118 |
sum([len(x) for x, y in NAME_ID_FORMATS.items()]) + \ |
|
119 |
len(NAME_ID_FORMATS) - 1 |
|
120 | 149 | |
121 | 150 |
def saml2_urn_to_nidformat(urn, accepted=()): |
122 | 151 |
for x, y in NAME_ID_FORMATS.items(): |
123 |
if accepted and not x in accepted:
|
|
152 |
if accepted and x not in accepted:
|
|
124 | 153 |
continue |
125 | 154 |
if y['samlv2'] == urn: |
126 | 155 |
return x |
127 | 156 |
return None |
128 | 157 | |
158 | ||
129 | 159 |
def nidformat_to_saml2_urn(key): |
130 | 160 |
return NAME_ID_FORMATS.get(key, {}).get('samlv2') |
131 | 161 | |
162 | ||
132 | 163 |
# According to: saml-profiles-2.0-os |
133 |
# The HTTP Redirect binding MUST NOT be used, as the response will typically exceed the URL length permitted by most user agents. |
|
164 |
# The HTTP Redirect binding MUST NOT be used, as the response will typically |
|
165 |
# exceed the URL length permitted by most user agents. |
|
134 | 166 |
BINDING_SSO_IDP = ( |
135 | 167 |
(lasso.SAML2_METADATA_BINDING_ARTIFACT, _('Artifact binding')), |
136 | 168 |
(lasso.SAML2_METADATA_BINDING_POST, _('POST binding')) |
... | ... | |
144 | 176 | |
145 | 177 | |
146 | 178 |
SIGNATURE_VERIFY_HINT = { |
147 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_MAYBE: _('Let authentic decides which signatures to check'),
|
|
148 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_FORCE: _('Always check signatures'),
|
|
149 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE: _('Does not check signatures') }
|
|
179 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_MAYBE: _('Let authentic decides which signatures to check'), |
|
180 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_FORCE: _('Always check signatures'), |
|
181 |
lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE: _('Does not check signatures') } |
|
150 | 182 | |
151 | 183 |
AUTHSAML2_UNAUTH_PERSISTENT = ( |
152 | 184 |
('AUTHSAML2_UNAUTH_PERSISTENT_ACCOUNT_LINKING_BY_AUTH', |
... | ... | |
169 | 201 |
Used to define SAML2 parameters employed with service providers. |
170 | 202 |
''' |
171 | 203 |
name = models.CharField(_('name'), max_length=80, unique=True) |
172 |
enabled = models.BooleanField(verbose_name = _('Enabled'), |
|
173 |
default=False, db_index=True) |
|
204 |
enabled = models.BooleanField(verbose_name=_('Enabled'), default=False, db_index=True) |
|
174 | 205 |
prefered_assertion_consumer_binding = models.CharField( |
175 |
verbose_name = _("Prefered assertion consumer binding"),
|
|
176 |
default = 'meta',
|
|
177 |
max_length = 4, choices = ASSERTION_CONSUMER_PROFILES)
|
|
178 |
encrypt_nameid = models.BooleanField(verbose_name = _("Encrypt NameID"),
|
|
179 |
default=False)
|
|
206 |
verbose_name=_("Prefered assertion consumer binding"),
|
|
207 |
default='meta',
|
|
208 |
max_length=4,
|
|
209 |
choices=ASSERTION_CONSUMER_PROFILES)
|
|
210 |
encrypt_nameid = models.BooleanField(verbose_name=_("Encrypt NameID"), default=False)
|
|
180 | 211 |
encrypt_assertion = models.BooleanField( |
181 |
verbose_name = _("Encrypt Assertion"),
|
|
182 |
default=False)
|
|
212 |
verbose_name=_("Encrypt Assertion"),
|
|
213 |
default=False) |
|
183 | 214 |
authn_request_signed = models.BooleanField( |
184 |
verbose_name = _("Authentication request signed"),
|
|
185 |
default=False)
|
|
215 |
verbose_name=_("Authentication request signed"),
|
|
216 |
default=False) |
|
186 | 217 |
idp_initiated_sso = models.BooleanField( |
187 |
verbose_name = _("Allow IdP initiated SSO"),
|
|
188 |
default=False, db_index=True)
|
|
218 |
verbose_name=_("Allow IdP initiated SSO"),
|
|
219 |
default=False, db_index=True) |
|
189 | 220 |
# XXX: format in the metadata file, should be suffixed with a star to mark |
190 | 221 |
# them as special |
191 |
default_name_id_format = models.CharField(max_length = 256, |
|
192 |
default = DEFAULT_NAME_ID_FORMAT, |
|
193 |
choices = NAME_ID_FORMATS_CHOICES) |
|
222 |
default_name_id_format = models.CharField( |
|
223 |
max_length=256, |
|
224 |
default=DEFAULT_NAME_ID_FORMAT, |
|
225 |
choices=NAME_ID_FORMATS_CHOICES) |
|
194 | 226 |
accepted_name_id_format = MultiSelectField( |
195 |
verbose_name = _("NameID formats accepted"), |
|
196 |
max_length=1024, |
|
197 |
blank=True, choices=NAME_ID_FORMATS_CHOICES) |
|
227 |
verbose_name=_("NameID formats accepted"), |
|
228 |
max_length=1024, |
|
229 |
blank=True, |
|
230 |
choices=NAME_ID_FORMATS_CHOICES) |
|
198 | 231 |
# TODO: add clean method which checks that the LassoProvider we can create |
199 | 232 |
# with the metadata file support the SP role |
200 | 233 |
# i.e. provider.roles & lasso.PROVIDER_ROLE_SP != 0 |
201 | 234 |
ask_user_consent = models.BooleanField( |
202 |
verbose_name = _('Ask user for consent when creating a federation'), default = False) |
|
203 |
accept_slo = models.BooleanField(\ |
|
204 |
verbose_name = _("Accept to receive Single Logout requests"), |
|
205 |
default=True, db_index=True) |
|
206 |
forward_slo = models.BooleanField(\ |
|
207 |
verbose_name = _("Forward Single Logout requests"), |
|
208 |
default=True) |
|
235 |
verbose_name=_('Ask user for consent when creating a federation'), default=False) |
|
236 |
accept_slo = models.BooleanField( |
|
237 |
verbose_name=_("Accept to receive Single Logout requests"), |
|
238 |
default=True, |
|
239 |
db_index=True) |
|
240 |
forward_slo = models.BooleanField( |
|
241 |
verbose_name=_("Forward Single Logout requests"), |
|
242 |
default=True) |
|
209 | 243 |
needs_iframe_logout = models.BooleanField( |
210 |
verbose_name=_('needs iframe logout'), |
|
211 |
help_text=_('logout URL are normally loaded inside an <img> HTML tag, some service provider need to use an iframe'), |
|
212 |
default=False) |
|
244 |
verbose_name=_('needs iframe logout'), |
|
245 |
help_text=_( |
|
246 |
'logout URL are normally loaded inside an <img> HTML tag, some service provider need to use an iframe'), |
|
247 |
default=False) |
|
213 | 248 |
iframe_logout_timeout = models.PositiveIntegerField( |
214 |
verbose_name=_('iframe logout timeout'), |
|
215 |
help_text=_('if iframe logout is used, it\'s the time between the ' |
|
216 |
'onload event for this iframe and the moment we consider its ' |
|
217 |
'loading to be really finished'), |
|
218 |
default=300) |
|
249 |
verbose_name=_('iframe logout timeout'), |
|
250 |
help_text=_( |
|
251 |
'if iframe logout is used, it\'s the time between the ' |
|
252 |
'onload event for this iframe and the moment we consider its ' |
|
253 |
'loading to be really finished'), |
|
254 |
default=300) |
|
219 | 255 |
http_method_for_slo_request = models.IntegerField( |
220 |
verbose_name = _("HTTP binding for the SLO requests"), |
|
221 |
choices = HTTP_METHOD, default = lasso.HTTP_METHOD_REDIRECT) |
|
222 |
federation_mode = models.PositiveIntegerField(_('federation mode'), |
|
223 |
choices=app_settings.FEDERATION_MODE.get_choices(app_settings), |
|
224 |
default=app_settings.FEDERATION_MODE.get_default(app_settings)) |
|
256 |
verbose_name=_("HTTP binding for the SLO requests"), |
|
257 |
choices=HTTP_METHOD, |
|
258 |
default=lasso.HTTP_METHOD_REDIRECT) |
|
259 |
federation_mode = models.PositiveIntegerField( |
|
260 |
_('federation mode'), |
|
261 |
choices=app_settings.FEDERATION_MODE.get_choices(app_settings), |
|
262 |
default=app_settings.FEDERATION_MODE.get_default(app_settings)) |
|
225 | 263 |
attributes = GenericRelation('SAMLAttribute') |
226 | 264 | |
227 | 265 |
objects = a2_managers.GetByNameManager() |
... | ... | |
236 | 274 |
def __str__(self): |
237 | 275 |
return self.name |
238 | 276 | |
277 | ||
239 | 278 |
@six.python_2_unicode_compatible |
240 | 279 |
class SAMLAttribute(models.Model): |
241 | 280 |
ATTRIBUTE_NAME_FORMATS = ( |
242 |
('basic', 'Basic'),
|
|
243 |
('uri', 'URI'),
|
|
244 |
('unspecified', 'Unspecified'),
|
|
281 |
('basic', 'Basic'), |
|
282 |
('uri', 'URI'), |
|
283 |
('unspecified', 'Unspecified'), |
|
245 | 284 |
) |
246 | 285 |
objects = managers.SAMLAttributeManager() |
247 | 286 | |
248 |
content_type = models.ForeignKey(ContentType, |
|
249 |
verbose_name=_('content type')) |
|
250 |
object_id = models.PositiveIntegerField( |
|
251 |
verbose_name=_('object identifier')) |
|
287 |
content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) |
|
288 |
object_id = models.PositiveIntegerField(verbose_name=_('object identifier')) |
|
252 | 289 |
provider = GenericForeignKey('content_type', 'object_id') |
253 | 290 |
name_format = models.CharField( |
254 |
max_length=64,
|
|
255 |
verbose_name=_('name format'),
|
|
256 |
default='basic',
|
|
257 |
choices=ATTRIBUTE_NAME_FORMATS)
|
|
291 |
max_length=64, |
|
292 |
verbose_name=_('name format'), |
|
293 |
default='basic', |
|
294 |
choices=ATTRIBUTE_NAME_FORMATS) |
|
258 | 295 |
name = models.CharField( |
259 |
max_length=128,
|
|
260 |
verbose_name=_('name'),
|
|
261 |
blank=True,
|
|
262 |
help_text=_('the local attribute name is used if left blank'))
|
|
296 |
max_length=128, |
|
297 |
verbose_name=_('name'), |
|
298 |
blank=True, |
|
299 |
help_text=_('the local attribute name is used if left blank')) |
|
263 | 300 |
friendly_name = models.CharField( |
264 |
max_length=64, |
|
265 |
verbose_name=_('friendly name'), |
|
266 |
blank=True) |
|
267 |
attribute_name = models.CharField(max_length=64, |
|
268 |
verbose_name=_('attribute name')) |
|
301 |
max_length=64, |
|
302 |
verbose_name=_('friendly name'), |
|
303 |
blank=True) |
|
304 |
attribute_name = models.CharField(max_length=64, verbose_name=_('attribute name')) |
|
269 | 305 |
enabled = models.BooleanField( |
270 |
verbose_name=_('enabled'),
|
|
271 |
default=True,
|
|
272 |
blank=True)
|
|
306 |
verbose_name=_('enabled'), |
|
307 |
default=True, |
|
308 |
blank=True) |
|
273 | 309 | |
274 | 310 |
def clean(self): |
275 | 311 |
super(SAMLAttribute, self).clean() |
... | ... | |
287 | 323 |
raise NotImplementedError |
288 | 324 | |
289 | 325 |
def to_tuples(self, ctx): |
290 |
if not self.attribute_name in ctx:
|
|
326 |
if self.attribute_name not in ctx:
|
|
291 | 327 |
return |
292 | 328 |
name_format = self.name_format_uri() |
293 | 329 |
name = self.name |
... | ... | |
302 | 338 |
def natural_key(self): |
303 | 339 |
if not hasattr(self.provider, 'natural_key'): |
304 | 340 |
return self.id |
305 |
return (self.content_type.natural_key(), self.provider.natural_key(), self.name_format, self.name, self.friendly_name, self.attribute_name) |
|
341 |
return (self.content_type.natural_key(), |
|
342 |
self.provider.natural_key(), |
|
343 |
self.name_format, |
|
344 |
self.name, |
|
345 |
self.friendly_name, |
|
346 |
self.attribute_name) |
|
306 | 347 | |
307 | 348 |
class Meta: |
308 |
unique_together = (('content_type', 'object_id', 'name_format', 'name', |
|
309 |
'friendly_name', 'attribute_name'),) |
|
349 |
unique_together = ( |
|
350 |
('content_type', 'object_id', 'name_format', 'name', 'friendly_name', 'attribute_name'), |
|
351 |
) |
|
310 | 352 | |
311 | 353 | |
312 | 354 |
@six.python_2_unicode_compatible |
313 | 355 |
class LibertyProvider(Service): |
314 |
entity_id = models.URLField(max_length=256, unique=True, |
|
315 |
verbose_name=_('Entity ID')) |
|
316 |
entity_id_sha1 = models.CharField(max_length=40, blank=True, |
|
317 |
verbose_name=_('Entity ID SHA1')) |
|
318 |
metadata_url = models.URLField(max_length=256, blank=True, |
|
319 |
verbose_name=_('Metadata URL')) |
|
356 |
entity_id = models.URLField(max_length=256, unique=True, verbose_name=_('Entity ID')) |
|
357 |
entity_id_sha1 = models.CharField(max_length=40, blank=True, verbose_name=_('Entity ID SHA1')) |
|
358 |
metadata_url = models.URLField(max_length=256, blank=True, verbose_name=_('Metadata URL')) |
|
320 | 359 |
protocol_conformance = models.IntegerField( |
321 |
choices=((lasso.PROTOCOL_SAML_2_0, 'SAML 2.0'),),
|
|
322 |
verbose_name=_('Protocol conformance'))
|
|
323 |
metadata = models.TextField(validators = [ metadata_validator ])
|
|
360 |
choices=((lasso.PROTOCOL_SAML_2_0, 'SAML 2.0'),), |
|
361 |
verbose_name=_('Protocol conformance')) |
|
362 |
metadata = models.TextField(validators=[metadata_validator])
|
|
324 | 363 |
# All following field must be PEM formatted textual data |
325 | 364 |
public_key = models.TextField(blank=True) |
326 | 365 |
ssl_certificate = models.TextField(blank=True) |
327 | 366 |
ca_cert_chain = models.TextField(blank=True) |
328 |
federation_source = models.CharField(max_length=64, blank=True, null=True, |
|
329 |
verbose_name=_('Federation source')) |
|
367 |
federation_source = models.CharField( |
|
368 |
max_length=64, |
|
369 |
blank=True, |
|
370 |
null=True, |
|
371 |
verbose_name=_('Federation source')) |
|
330 | 372 | |
331 | 373 |
attributes = GenericRelation(SAMLAttribute) |
332 | 374 | |
... | ... | |
338 | 380 |
def save(self, *args, **kwargs): |
339 | 381 |
'''Update the SHA1 hash of the entity_id when saving''' |
340 | 382 |
if self.protocol_conformance == 3: |
341 |
self.entity_id_sha1 = hashlib.sha1(self.entity_id.encode('ascii')) \ |
|
342 |
.hexdigest() |
|
383 |
self.entity_id_sha1 = hashlib.sha1(self.entity_id.encode('ascii')).hexdigest() |
|
343 | 384 |
super(LibertyProvider, self).save(*args, **kwargs) |
344 | 385 | |
345 | 386 |
def clean(self): |
... | ... | |
374 | 415 |
verbose_name = _('SAML provider') |
375 | 416 |
verbose_name_plural = _('SAML providers') |
376 | 417 | |
418 | ||
377 | 419 |
def get_all_custom_or_default(instance, name): |
378 | 420 |
model = instance._meta.get_field(name).rel.to |
379 | 421 |
try: |
... | ... | |
388 | 430 |
except ObjectDoesNotExist: |
389 | 431 |
raise RuntimeError('Default %s is missing' % model) |
390 | 432 | |
433 | ||
391 | 434 |
# TODO: The IdP must look to the preferred binding order for sso in the SP metadata (AssertionConsumerService) |
392 | 435 |
# expect if the protocol for response is defined in the request (ProtocolBinding attribute) |
393 | 436 |
@six.python_2_unicode_compatible |
394 | 437 |
class LibertyServiceProvider(models.Model): |
395 |
liberty_provider = models.OneToOneField(LibertyProvider, |
|
396 |
primary_key = True, related_name = 'service_provider') |
|
397 |
enabled = models.BooleanField(verbose_name = _('Enabled'), |
|
398 |
default=False, db_index=True) |
|
399 |
enable_following_sp_options_policy = models.BooleanField(verbose_name = \ |
|
400 |
_('The following options policy will apply except if a policy for all service provider is defined.'), |
|
438 |
liberty_provider = models.OneToOneField(LibertyProvider, primary_key=True, related_name='service_provider') |
|
439 |
enabled = models.BooleanField(verbose_name=_('Enabled'), default=False, db_index=True) |
|
440 |
enable_following_sp_options_policy = models.BooleanField( |
|
441 |
verbose_name=_('The following options policy will apply except ' |
|
442 |
'if a policy for all service provider is defined.'), |
|
401 | 443 |
default=False) |
402 |
sp_options_policy = models.ForeignKey(SPOptionsIdPPolicy, |
|
403 |
related_name="sp_options_policy", |
|
404 |
verbose_name=_('service provider options policy'), blank=True, |
|
405 |
null=True, |
|
406 |
on_delete=models.SET_NULL) |
|
444 |
sp_options_policy = models.ForeignKey( |
|
445 |
SPOptionsIdPPolicy, |
|
446 |
related_name="sp_options_policy", |
|
447 |
verbose_name=_('service provider options policy'), blank=True, |
|
448 |
null=True, |
|
449 |
on_delete=models.SET_NULL) |
|
407 | 450 |
users_can_manage_federations = models.BooleanField( |
408 |
verbose_name=_('users can manage federation'),
|
|
409 |
default=True,
|
|
410 |
blank=True,
|
|
411 |
db_index=True)
|
|
451 |
verbose_name=_('users can manage federation'), |
|
452 |
default=True, |
|
453 |
blank=True, |
|
454 |
db_index=True) |
|
412 | 455 | |
413 | 456 |
objects = managers.GetByLibertyProviderManager() |
414 | 457 | |
... | ... | |
425 | 468 | |
426 | 469 |
LIBERTY_SESSION_DUMP_KIND_SP = 0 |
427 | 470 |
LIBERTY_SESSION_DUMP_KIND_IDP = 1 |
428 |
LIBERTY_SESSION_DUMP_KIND = { LIBERTY_SESSION_DUMP_KIND_SP: 'sp', |
|
429 |
LIBERTY_SESSION_DUMP_KIND_IDP: 'idp' } |
|
471 |
LIBERTY_SESSION_DUMP_KIND = { |
|
472 |
LIBERTY_SESSION_DUMP_KIND_SP: 'sp', |
|
473 |
LIBERTY_SESSION_DUMP_KIND_IDP: 'idp', |
|
474 |
} |
|
475 | ||
430 | 476 | |
431 | 477 |
class LibertySessionDump(models.Model): |
432 | 478 |
'''Store lasso session object dump. |
433 | 479 | |
434 | 480 |
Should be replaced in the future by direct references to known |
435 | 481 |
assertions through the LibertySession object''' |
436 |
django_session_key = models.CharField(max_length = 128)
|
|
437 |
session_dump = models.TextField(blank = True)
|
|
438 |
kind = models.IntegerField(choices = LIBERTY_SESSION_DUMP_KIND.items())
|
|
482 |
django_session_key = models.CharField(max_length=128)
|
|
483 |
session_dump = models.TextField(blank=True)
|
|
484 |
kind = models.IntegerField(choices=LIBERTY_SESSION_DUMP_KIND.items())
|
|
439 | 485 | |
440 | 486 |
objects = managers.SessionLinkedManager() |
441 | 487 | |
... | ... | |
444 | 490 |
verbose_name_plural = _('SAML session dumps') |
445 | 491 |
unique_together = (('django_session_key', 'kind'),) |
446 | 492 | |
493 | ||
447 | 494 |
class LibertyArtifact(models.Model): |
448 | 495 |
"""Store an artifact and the associated XML content""" |
449 | 496 |
creation = models.DateTimeField(auto_now_add=True) |
450 |
artifact = models.CharField(max_length = 128, primary_key = True)
|
|
497 |
artifact = models.CharField(max_length=128, primary_key=True)
|
|
451 | 498 |
content = models.TextField() |
452 |
provider_id = models.CharField(max_length = 256)
|
|
499 |
provider_id = models.CharField(max_length=256)
|
|
453 | 500 | |
454 | 501 |
objects = managers.LibertyArtifactManager() |
455 | 502 | |
... | ... | |
457 | 504 |
verbose_name = _('SAML artifact') |
458 | 505 |
verbose_name_plural = _('SAML artifacts') |
459 | 506 | |
507 | ||
460 | 508 |
def nameid2kwargs(name_id): |
461 | 509 |
return { |
462 | 510 |
'name_id_qualifier': name_id.nameQualifier, |
463 | 511 |
'name_id_sp_name_qualifier': name_id.spNameQualifier, |
464 | 512 |
'name_id_content': name_id.content, |
465 |
'name_id_format': name_id.format } |
|
513 |
'name_id_format': name_id.format, |
|
514 |
} |
|
466 | 515 | |
467 | 516 |
# XXX: for retrocompatibility |
468 | 517 |
federation_delete = managers.federation_delete |
469 | 518 | |
519 | ||
470 | 520 |
@six.python_2_unicode_compatible |
471 | 521 |
class LibertyFederation(models.Model): |
472 | 522 |
"""Store a federation, i.e. an identifier shared with another provider, be |
473 | 523 |
it IdP or SP""" |
474 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, |
|
475 |
on_delete=models.SET_NULL) |
|
524 |
user = models.ForeignKey( |
|
525 |
settings.AUTH_USER_MODEL, |
|
526 |
null=True, |
|
527 |
blank=True, |
|
528 |
on_delete=models.SET_NULL) |
|
476 | 529 |
sp = models.ForeignKey('LibertyServiceProvider', null=True, blank=True) |
477 |
name_id_format = models.CharField(max_length = 100, |
|
478 |
verbose_name = "NameIDFormat", blank=True, null=True) |
|
479 |
name_id_content = models.CharField(max_length = 100, |
|
480 |
verbose_name = "NameID") |
|
481 |
name_id_qualifier = models.CharField(max_length = 256, |
|
482 |
verbose_name = "NameQualifier", blank=True, null=True) |
|
483 |
name_id_sp_name_qualifier = models.CharField(max_length = 256, |
|
484 |
verbose_name = "SPNameQualifier", blank=True, null=True) |
|
530 |
name_id_format = models.CharField(max_length=100, verbose_name="NameIDFormat", blank=True, null=True) |
|
531 |
name_id_content = models.CharField(max_length=100, verbose_name="NameID") |
|
532 |
name_id_qualifier = models.CharField(max_length=256, verbose_name="NameQualifier", blank=True, null=True) |
|
533 |
name_id_sp_name_qualifier = models.CharField(max_length=256, verbose_name="SPNameQualifier", blank=True, null=True) |
|
485 | 534 |
termination_notified = models.BooleanField(blank=True, default=False) |
486 | 535 |
creation = models.DateTimeField(auto_now_add=True) |
487 | 536 |
last_modification = models.DateTimeField(auto_now=True) |
... | ... | |
507 | 556 |
key += (None,) |
508 | 557 |
return key |
509 | 558 | |
510 | ||
511 | 559 |
def is_unique(self, for_format=True): |
512 | 560 |
'''Return whether a federation already exist for this user and this provider. |
513 | 561 | |
514 | 562 |
By default the check is made by name_id_format, if you want to check |
515 | 563 |
whatever the format, set for_format to False. |
516 | 564 |
''' |
517 |
qs = LibertyFederation.objects.exclude(id=self.id) \ |
|
518 |
.filter(user=self.user, idp=self.idp, sp=self.sp) |
|
565 |
qs = LibertyFederation.objects.exclude(id=self.id).filter(user=self.user, idp=self.idp, sp=self.sp) |
|
519 | 566 |
if for_format: |
520 | 567 |
qs = qs.filter(name_id_format=self.name_id_format) |
521 | 568 |
return not qs.exists() |
... | ... | |
531 | 578 |
@six.python_2_unicode_compatible |
532 | 579 |
class LibertySession(models.Model): |
533 | 580 |
"""Store the link between a Django session and a SAML session""" |
534 |
django_session_key = models.CharField(max_length = 128) |
|
535 |
session_index = models.CharField(max_length = 80) |
|
536 |
provider_id = models.CharField(max_length = 256) |
|
537 |
federation = models.ForeignKey(LibertyFederation, blank=True, |
|
538 |
null = True) |
|
539 |
name_id_qualifier = models.CharField(max_length = 256, |
|
540 |
verbose_name = _("Qualifier"), null = True) |
|
541 |
name_id_format = models.CharField(max_length = 100, |
|
542 |
verbose_name = _("NameIDFormat"), null = True) |
|
543 |
name_id_content = models.CharField(max_length = 100, |
|
544 |
verbose_name = _("NameID")) |
|
545 |
name_id_sp_name_qualifier = models.CharField(max_length = 256, |
|
546 |
verbose_name = _("SPNameQualifier"), null = True) |
|
581 |
django_session_key = models.CharField(max_length=128) |
|
582 |
session_index = models.CharField(max_length=80) |
|
583 |
provider_id = models.CharField(max_length=256) |
|
584 |
federation = models.ForeignKey(LibertyFederation, blank=True, null=True) |
|
585 |
name_id_qualifier = models.CharField(max_length=256, verbose_name=_("Qualifier"), null=True) |
|
586 |
name_id_format = models.CharField(max_length=100, verbose_name=_("NameIDFormat"), null=True) |
|
587 |
name_id_content = models.CharField(max_length=100, verbose_name=_("NameID")) |
|
588 |
name_id_sp_name_qualifier = models.CharField(max_length=256, verbose_name=_("SPNameQualifier"), null=True) |
|
547 | 589 |
creation = models.DateTimeField(auto_now_add=True) |
548 | 590 | |
549 | 591 |
objects = managers.LibertySessionManager() |
... | ... | |
567 | 609 |
return LibertySession.objects.none() |
568 | 610 |
kwargs = nameid2kwargs(name_id) |
569 | 611 |
name_id_qualifier = kwargs['name_id_qualifier'] |
570 |
qs = LibertySession.objects.filter(provider_id=provider_id, |
|
571 |
session_index__in=session_indexes) |
|
612 |
qs = LibertySession.objects.filter(provider_id=provider_id, session_index__in=session_indexes) |
|
572 | 613 |
if name_id_qualifier and name_id_qualifier != issuer_id: |
573 | 614 |
qs = qs.filter(**kwargs) |
574 | 615 |
else: |
575 | 616 |
kwargs.pop('name_id_qualifier') |
576 |
qs = qs.filter(**kwargs) \ |
|
577 |
.filter(Q(name_id_qualifier__isnull=True)|Q(name_id_qualifier=issuer_id)) |
|
578 |
qs = qs.filter(Q(name_id_sp_name_qualifier__isnull=True)|Q(name_id_sp_name_qualifier=provider_id)) |
|
617 |
qs = qs.filter(**kwargs).filter(Q(name_id_qualifier__isnull=True) | Q(name_id_qualifier=issuer_id)) |
|
618 |
qs = qs.filter(Q(name_id_sp_name_qualifier__isnull=True) | Q(name_id_sp_name_qualifier=provider_id)) |
|
579 | 619 |
return qs |
580 | 620 | |
581 | 621 |
def __str__(self): |
... | ... | |
585 | 625 |
verbose_name = _("SAML session") |
586 | 626 |
verbose_name_plural = _("SAML sessions") |
587 | 627 | |
628 | ||
588 | 629 |
@six.python_2_unicode_compatible |
589 | 630 |
class KeyValue(models.Model): |
590 | 631 |
key = models.CharField(max_length=128, primary_key=True) |
... | ... | |
600 | 641 |
verbose_name = _("key value association") |
601 | 642 |
verbose_name_plural = _("key value associations") |
602 | 643 | |
644 | ||
603 | 645 |
def save_key_values(key, *values): |
604 | 646 |
# never update an existing key, key are nonces |
605 | 647 |
kv, created = KeyValue.objects.get_or_create(key=key, defaults={'value': values}) |
... | ... | |
607 | 649 |
kv.value = values |
608 | 650 |
kv.save() |
609 | 651 | |
652 | ||
610 | 653 |
def get_and_delete_key_values(key): |
611 | 654 |
try: |
612 | 655 |
kv = KeyValue.objects.get(key=key) |
src/authentic2/saml/saml11utils.py | ||
---|---|---|
1 |
from __future__ import print_function |
|
2 | ||
3 |
import xml.etree.ElementTree as etree |
|
4 |
from authentic2.compat_lasso import lasso |
|
5 |
from authentic2.saml import x509utils |
|
6 |
from authentic2.saml.saml2utils import bool2xs, NamespacedTreeBuilder, keyinfo |
|
7 | ||
8 |
class Saml11Metadata(object): |
|
9 |
ENTITY_DESCRIPTOR = 'EntityDescriptor' |
|
10 |
SP_SSO_DESCRIPTOR = 'SPDescriptor' |
|
11 |
IDP_SSO_DESCRIPTOR = 'IDPDescriptor' |
|
12 |
PROTOCOL_SUPPORT_ENUMERATION = 'protocolSupportEnumeration' |
|
13 |
SOAP_ENDPOINT = 'SoapEndpoint' |
|
14 |
PROVIDER_ID = 'providerID' |
|
15 |
VALID_UNTIL = 'validUntil' |
|
16 |
CACHE_DURATION = 'cacheDuration' |
|
17 |
ENCRYPTION_METHOD = 'EncryptionMethod' |
|
18 |
KEY_SIZE = 'KeySize' |
|
19 |
KEY_DESCRIPTOR = 'KeyDescriptor' |
|
20 |
SERVICE_URL = "ServiceURL" |
|
21 |
SERVICE_RETURN_URL = "ServiceReturnURL" |
|
22 |
USE = 'use' |
|
23 |
PROTOCOL_PROFILE = 'ProtocolProfile' |
|
24 |
FEDERATION_TERMINATION_NOTIFICATION_PROTOCOL_PROFILE = \ |
|
25 |
'FederationTerminationNotificationProtocolProfile' |
|
26 |
AUTHN_REQUESTS_SIGNED = 'AuthnRequestsSigned' |
|
27 |
# Service prefixes |
|
28 |
SINGLE_LOGOUT = 'SingleLogout' |
|
29 |
FEDERATION_TERMINATION = 'FederationTermination' |
|
30 |
REGISTER_NAME_IDENTIFIER = 'RegisterNameIdentifier' |
|
31 |
# SP Services prefixes |
|
32 |
ASSERTION_CONSUMER = 'AssertionConsumer' |
|
33 |
# IDP Services prefixes |
|
34 |
SINGLE_SIGN_ON = 'SingleSignOn' |
|
35 |
AUTHN = 'Authn' |
|
36 | ||
37 |
sso_services = ( SOAP_ENDPOINT, SINGLE_LOGOUT, FEDERATION_TERMINATION, |
|
38 |
REGISTER_NAME_IDENTIFIER ) |
|
39 |
idp_services = ( SINGLE_SIGN_ON, AUTHN) |
|
40 |
sp_services = ( ASSERTION_CONSUMER, AUTHN_REQUESTS_SIGNED) |
|
41 | ||
42 |
def __init__(self, entity_id, url_prefix = '', valid_until = None, |
|
43 |
cache_duration = None, protocol_support_enumeration = []): |
|
44 |
'''Initialize a new generator for a metadata file. |
|
45 | ||
46 |
Entity id is the name of the provider |
|
47 |
''' |
|
48 |
self.entity_id = entity_id |
|
49 |
self.url_prefix = url_prefix |
|
50 |
self.role_descriptors = {} |
|
51 |
self.valid_until = valid_until |
|
52 |
self.cache_duration = cache_duration |
|
53 |
self.tb = NamespacedTreeBuilder() |
|
54 |
self.tb.pushNamespace(lasso.METADATA_HREF) |
|
55 |
if not protocol_support_enumeration: |
|
56 |
raise TypeError('Protocol Support Enumeration is mandatory') |
|
57 |
self.protocol_support_enumeration = protocol_support_enumeration |
|
58 | ||
59 |
def add_role_descriptor(self, role, map, options): |
|
60 |
'''Add a role descriptor, map is a sequence of tuples formatted as |
|
61 | ||
62 |
(endpoint_type, (bindings, ..) , url [, return_url])''' |
|
63 |
if not self.SOAP_ENDPOINT in map: |
|
64 |
raise TypeError('SoapEndpoint is mandatory in SAML 1.1 role descriptors') |
|
65 |
self.role_descriptors[role] = (map, options) |
|
66 | ||
67 |
def add_sp_descriptor(self, map, options): |
|
68 |
if not self.ASSERTION_CONSUMER in map: |
|
69 |
raise TypeError('AssertionConsumer is mandarotyr in SAML 1.1 SP role descriptors') |
|
70 |
for row in map: |
|
71 |
if row not in self.sp_services + self.sso_services: |
|
72 |
raise TypeError(row) |
|
73 |
self.add_role_descriptor('sp', map, options) |
|
74 | ||
75 |
def add_idp_descriptor(self, map, options): |
|
76 |
if not self.SINGLE_SIGN_ON in map: |
|
77 |
raise TypeError('SingleSignOn is mandarotyr in SAML 1.1 SP role descriptors') |
|
78 |
for row in map: |
|
79 |
if row not in self.idp_services + self.sso_services: |
|
80 |
raise TypeError(row) |
|
81 |
self.add_role_descriptor('idp', map, options) |
|
82 | ||
83 |
def add_keyinfo(self, key, use, encryption_method = None, key_size = None): |
|
84 |
attrib = {} |
|
85 |
if use: |
|
86 |
attrib = { self.USE: use } |
|
87 |
self.tb.start(self.KEY_DESCRIPTOR, attrib) |
|
88 |
if encryption_method: |
|
89 |
self.tb.simple_content(self.ENCRYPTION_METHOD, encryption_method) |
|
90 |
if key_size: |
|
91 |
self.tb.simple_content(self.KEY_SIZE, str(key_size)) |
|
92 |
keyinfo(self.tb, key) |
|
93 |
self.tb.end(self.KEY_DESCRIPTOR) |
|
94 | ||
95 |
def add_service_url(self, name, map): |
|
96 |
service = map.get(name) |
|
97 |
if service: |
|
98 |
service_urls = service[0] |
|
99 |
self.tb.simple_content(name + self.SERVICE_URL, |
|
100 |
self.url_prefix + service_urls[0]) |
|
101 |
if len(service_urls) == 2: |
|
102 |
self.tb.simple_content(name + self.SERVICE_RETURN_URL, |
|
103 |
self.url_prefix + service_urls[1]) |
|
104 | ||
105 |
def add_profile(self, name, map, tag = None): |
|
106 |
if not tag: |
|
107 |
tag = name + self.PROTOCOL_PROFILE |
|
108 |
service = map.get(name) |
|
109 |
if service: |
|
110 |
service_profiles = service[1] |
|
111 |
for profile in service_profiles: |
|
112 |
self.tb.simple_content(tag, profile) |
|
113 | ||
114 |
def generate_sso_descriptor(self, name, map, options): |
|
115 |
attrib = {} |
|
116 | ||
117 |
if options.get('valid_until'): |
|
118 |
attrib[self.VALID_UNTIL] = options['valid_until'] |
|
119 |
if options.get('cached_duration'): |
|
120 |
attrib[self.CACHE_DURATION] = options['cache_duration'] |
|
121 |
attrib[self.PROTOCOL_SUPPORT_ENUMERATION] = options[self.PROTOCOL_SUPPORT_ENUMERATION] |
|
122 |
self.tb.start(name, attrib) |
|
123 |
# Add KeyDescriptor(s) |
|
124 |
if options.get('signing_key'): |
|
125 |
self.add_keyinfo(options['signing_key'], 'signing',) |
|
126 |
if options.get('encryption_key'): |
|
127 |
self.add_keyinfo(options['encryption_key'], 'encryption', |
|
128 |
encryption_method = options.get('encryption_method'), |
|
129 |
key_size = options.get('key_size')) |
|
130 |
if options.get('key'): |
|
131 |
self.add_keyinfo(options['encryption_key'], 'signing encryption', |
|
132 |
encryption_method = options.get('encryption_method'), |
|
133 |
key_size = options.get('key_size')) |
|
134 |
# Add SOAP Endpoint |
|
135 |
self.tb.simple_content(self.SOAP_ENDPOINT, |
|
136 |
self.url_prefix + map[self.SOAP_ENDPOINT]) |
|
137 |
# Add SingleLogoutService |
|
138 |
self.add_service_url(self.SINGLE_LOGOUT, map) |
|
139 |
# Add FederationTerminationService URL |
|
140 |
self.add_service_url(self.FEDERATION_TERMINATION, map) |
|
141 |
self.add_profile(self.FEDERATION_TERMINATION, map, |
|
142 |
tag = self.FEDERATION_TERMINATION_NOTIFICATION_PROTOCOL_PROFILE) |
|
143 |
# Add SingleLogoutProtocolProfile |
|
144 |
self.add_profile(self.SINGLE_LOGOUT, map) |
|
145 |
# Add RegisterNameIdentifier |
|
146 |
self.add_profile(self.REGISTER_NAME_IDENTIFIER, map) |
|
147 |
self.add_service_url(self.REGISTER_NAME_IDENTIFIER, map) |
|
148 | ||
149 |
def generate_idp_descriptor(self, map, options): |
|
150 |
self.generate_sso_descriptor(self.IDP_SSO_DESCRIPTOR, map, options) |
|
151 |
# Add SingleSignOnServiceURL |
|
152 |
self.add_service_url(self.SINGLE_SIGN_ON, map) |
|
153 |
self.add_profile(self.SINGLE_SIGN_ON, map) |
|
154 |
# Add AuthnServiceURL |
|
155 |
self.add_service_url(self.AUTHN, map) |
|
156 |
self.tb.end(self.IDP_SSO_DESCRIPTOR) |
|
157 | ||
158 |
def generate_sp_descriptor(self, map, options): |
|
159 |
self.generate_sso_descriptor(self.SP_SSO_DESCRIPTOR, map, options) |
|
160 |
# Add AssertionConsumerServiceURL |
|
161 |
self.add_service_url(self.ASSERTION_CONSUMER) |
|
162 |
self.simple_content(self.AUTHN_REQUESTS_SIGNED, |
|
163 |
bool2xs(options.get(self.AUTHN_REQUESTS_SIGNED, False))) |
|
164 |
self.tb.end(self.SP_SSO_DESCRIPTOR) |
|
165 | ||
166 |
def root_element(self): |
|
167 |
attrib = { self.PROVIDER_ID : self.entity_id} |
|
168 |
if self.cache_duration: |
|
169 |
attrib['cacheDuration'] = self.cache_duration |
|
170 |
if self.valid_until: |
|
171 |
attrib['validUntil'] = self.valid_until |
|
172 |
self.entity_descriptor = self.tb.start(self.ENTITY_DESCRIPTOR, attrib) |
|
173 |
# Generate sso descriptor |
|
174 |
attrib = { self.PROTOCOL_SUPPORT_ENUMERATION: ' '.join(self.protocol_support_enumeration) } |
|
175 |
if self.role_descriptors.get('idp'): |
|
176 |
map, options = self.role_descriptors['idp'] |
|
177 |
options.update(attrib) |
|
178 |
self.generate_idp_descriptor(map, options) |
|
179 |
if self.role_descriptors.get('sp'): |
|
180 |
map, options = self.role_descriptors['sp'] |
|
181 |
options.update(attrib) |
|
182 |
self.generate_idp_sso_descriptor(map, options) |
|
183 |
self.tb.end(self.ENTITY_DESCRIPTOR) |
|
184 |
return self.tb.close() |
|
185 | ||
186 |
def __str__(self): |
|
187 |
return '<?xml version="1.0"?>\n' + etree.tostring(self.root_element()) |
|
188 | ||
189 |
if __name__ == '__main__': |
|
190 |
pkey, _ = x509utils.generate_rsa_keypair() |
|
191 |
meta = Saml11Metadata('http://example.com/saml', |
|
192 |
'http://example.com/saml/prefix/', |
|
193 |
protocol_support_enumeration = [ lasso.LIB_HREF ]) |
|
194 |
sso_protocol_profiles = [ |
|
195 |
lasso.LIB_PROTOCOL_PROFILE_BRWS_ART, |
|
196 |
lasso.LIB_PROTOCOL_PROFILE_BRWS_POST, |
|
197 |
lasso.LIB_PROTOCOL_PROFILE_BRWS_LECP ] |
|
198 |
slo_protocol_profiles = [ |
|
199 |
lasso.LIB_PROTOCOL_PROFILE_SLO_SP_HTTP, |
|
200 |
lasso.LIB_PROTOCOL_PROFILE_SLO_SP_SOAP, |
|
201 |
lasso.LIB_PROTOCOL_PROFILE_SLO_IDP_HTTP, |
|
202 |
lasso.LIB_PROTOCOL_PROFILE_SLO_IDP_SOAP ] |
|
203 |
options = { 'signing_key': pkey } |
|
204 |
meta.add_idp_descriptor({ |
|
205 |
'SoapEndpoint': 'soap', |
|
206 |
'SingleLogout': (('slo', 'sloReturn'), slo_protocol_profiles), |
|
207 |
'SingleSignOn': (('sso',), sso_protocol_profiles), |
|
208 |
}, options) |
|
209 |
root = meta.root_element() |
|
210 |
print(etree.tostring(root)) |
src/authentic2/saml/saml2utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import xml.etree.ElementTree as etree |
... | ... | |
22 | 38 | |
23 | 39 |
def filter_element_private_key(message): |
24 | 40 |
if isinstance(message, six.string_types): |
25 |
return re.sub(r'(<saml)(p)?(:PrivateKeyFile>-----BEGIN RSA PRIVATE KEY-----)' |
|
26 |
'([&#;\w/+=\s])+' |
|
27 |
'(-----END RSA PRIVATE KEY-----</saml)(p)?(:PrivateKeyFile>)', |
|
41 |
return re.sub( |
|
42 |
r'(<saml)(p)?(:PrivateKeyFile>-----BEGIN RSA PRIVATE KEY-----)' |
|
43 |
r'([&#;\w/+=\s])+' |
|
44 |
r'(-----END RSA PRIVATE KEY-----</saml)(p)?(:PrivateKeyFile>)', |
|
28 | 45 |
'', message) |
29 | 46 |
else: |
30 | 47 |
return message |
... | ... | |
38 | 55 |
return 'false' |
39 | 56 |
raise TypeError() |
40 | 57 | |
58 | ||
41 | 59 |
def int_to_b64(i): |
42 | 60 |
h = hex(i)[2:].strip('L') |
43 | 61 |
if len(h) % 2 == 1: |
44 | 62 |
h = '0' + h |
45 | 63 |
return base64.b64encode(binascii.unhexlify(h)) |
46 | 64 | |
65 | ||
47 | 66 |
def keyinfo(tb, key): |
48 | 67 |
tb.pushNamespace(lasso.DS_HREF) |
49 | 68 |
tb.start('KeyInfo', {}) |
... | ... | |
68 | 87 |
tb.end('KeyInfo') |
69 | 88 |
tb.popNamespace() |
70 | 89 | |
90 | ||
71 | 91 |
class NamespacedTreeBuilder(etree.TreeBuilder): |
72 | 92 |
def __init__(self, *args, **kwargs): |
73 | 93 |
self.__old_ns = [] |
... | ... | |
92 | 112 |
self.data(data) |
93 | 113 |
self.end() |
94 | 114 | |
95 |
def end(self, tag = None):
|
|
115 |
def end(self, tag=None):
|
|
96 | 116 |
if tag: |
97 | 117 |
self.__opened.pop() |
98 | 118 |
tag = '{%s}%s' % (self.__ns, tag) |
... | ... | |
100 | 120 |
tag = self.__opened.pop() |
101 | 121 |
return etree.TreeBuilder.end(self, tag) |
102 | 122 | |
123 | ||
103 | 124 |
class Saml2Metadata(object): |
104 | 125 |
ENTITY_DESCRIPTOR = 'EntityDescriptor' |
105 | 126 |
SP_SSO_DESCRIPTOR = 'SPSSODescriptor' |
... | ... | |
118 | 139 |
DISCOVERY_NS = 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol' |
119 | 140 |
DISCOVERY_BINDING = 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol' |
120 | 141 | |
121 |
sso_services = ( ARTIFACT_RESOLUTION_SERVICE, SINGLE_LOGOUT_SERVICE, |
|
122 |
MANAGE_NAME_ID_SERVICE ) |
|
123 |
idp_services = ( SINGLE_SIGN_ON_SERVICE, NAME_ID_MAPPING_SERVICE, |
|
124 |
ASSERTION_ID_REQUEST_SERVICE ) |
|
125 |
sp_services = ( ASSERTION_CONSUMER_SERVICE, ) |
|
126 |
indexed_endpoints = ( ARTIFACT_RESOLUTION_SERVICE, |
|
127 |
ASSERTION_CONSUMER_SERVICE ) |
|
142 |
sso_services = (ARTIFACT_RESOLUTION_SERVICE, SINGLE_LOGOUT_SERVICE, MANAGE_NAME_ID_SERVICE) |
|
143 |
idp_services = (SINGLE_SIGN_ON_SERVICE, NAME_ID_MAPPING_SERVICE, ASSERTION_ID_REQUEST_SERVICE) |
|
144 |
sp_services = (ASSERTION_CONSUMER_SERVICE,) |
|
145 |
indexed_endpoints = (ARTIFACT_RESOLUTION_SERVICE, ASSERTION_CONSUMER_SERVICE) |
|
128 | 146 | |
129 |
def __init__(self, entity_id, url_prefix = '', valid_until = None, |
|
130 |
cache_duration = None): |
|
147 |
def __init__(self, entity_id, url_prefix='', valid_until=None, cache_duration=None): |
|
131 | 148 |
'''Initialize a new generator for a metadata file. |
132 | 149 | |
133 | 150 |
Entity id is the name of the provider |
... | ... | |
184 | 201 |
self.add_keyinfo(options['key'], None) |
185 | 202 |
if 'disco' in options: |
186 | 203 |
self.add_disco_extension(options['disco']) |
187 |
endpoint_idx = collections.defaultdict(lambda:0) |
|
204 |
endpoint_idx = collections.defaultdict(lambda: 0)
|
|
188 | 205 |
for service in listing: |
189 |
selected = [ row for row in map if row[0] == service ]
|
|
206 |
selected = [row for row in map if row[0] == service]
|
|
190 | 207 |
for row in selected: |
191 | 208 |
if isinstance(row[1], str): |
192 |
bindings = [ row[1] ]
|
|
209 |
bindings = [row[1]]
|
|
193 | 210 |
else: |
194 | 211 |
bindings = row[1] |
195 | 212 |
for binding in bindings: |
196 |
attribs = { 'Binding' : binding, |
|
197 |
'Location': self.url_prefix + row[2] } |
|
213 |
attribs = { |
|
214 |
'Binding': binding, |
|
215 |
'Location': self.url_prefix + row[2] |
|
216 |
} |
|
198 | 217 |
if len(row) == 4: |
199 | 218 |
attribs['ResponseLocation'] = self.url_prefix + row[3] |
200 | 219 |
if service in self.indexed_endpoints: |
... | ... | |
211 | 230 |
def add_keyinfo(self, key, use): |
212 | 231 |
attrib = {} |
213 | 232 |
if use: |
214 |
attrib = { 'use': use }
|
|
233 |
attrib = {'use': use}
|
|
215 | 234 |
self.tb.start(self.KEY_DESCRIPTOR, attrib) |
216 | 235 |
keyinfo(self.tb, key) |
217 | 236 |
self.tb.end(self.KEY_DESCRIPTOR) |
218 | 237 | |
219 | 238 |
def root_element(self): |
220 |
attrib = { 'entityID' : self.entity_id}
|
|
239 |
attrib = {'entityID': self.entity_id}
|
|
221 | 240 |
if self.cache_duration: |
222 | 241 |
attrib['cacheDuration'] = self.cache_duration |
223 | 242 |
if self.valid_until: |
... | ... | |
225 | 244 | |
226 | 245 |
self.entity_descriptor = self.tb.start(self.ENTITY_DESCRIPTOR, attrib) |
227 | 246 |
# Generate sso descriptor |
228 |
attrib = { self.PROTOCOL_SUPPORT_ENUMERATION: lasso.SAML2_PROTOCOL_HREF }
|
|
247 |
attrib = {self.PROTOCOL_SUPPORT_ENUMERATION: lasso.SAML2_PROTOCOL_HREF}
|
|
229 | 248 |
if self.role_descriptors.get('sp'): |
230 | 249 |
map, options = self.role_descriptors['sp'] |
231 | 250 |
self.sp_descriptor = self.tb.start(self.SP_SSO_DESCRIPTOR, attrib) |
... | ... | |
246 | 265 |
self.tb.pushNamespace(self.DISCOVERY_NS) |
247 | 266 |
index = 0 |
248 | 267 |
for url in disco_return_url: |
249 |
attrib = {'Binding': self.DISCOVERY_BINDING, |
|
268 |
attrib = { |
|
269 |
'Binding': self.DISCOVERY_BINDING, |
|
250 | 270 |
'Location': self.url_prefix + url, |
251 |
'index': str(index)} |
|
271 |
'index': str(index) |
|
272 |
} |
|
252 | 273 |
self.tb.start(self.DISCOVERY_RESPONSE, attrib) |
253 | 274 |
self.tb.end(self.DISCOVERY_RESPONSE) |
254 | 275 |
index += 1 |
... | ... | |
258 | 279 |
def __str__(self): |
259 | 280 |
return '<?xml version="1.0"?>\n' + etree.tostring(self.root_element()) |
260 | 281 | |
282 | ||
261 | 283 |
def iso8601_to_datetime(date_string): |
262 | 284 |
'''Convert a string formatted as an ISO8601 date into a time_t value. |
263 | 285 | |
... | ... | |
265 | 287 |
m = re.match(r'(\d+-\d+-\d+T\d+:\d+:\d+)(?:\.\d+)?Z$', date_string) |
266 | 288 |
if not m: |
267 | 289 |
raise ValueError('Invalid ISO8601 date') |
268 |
tm = time.strptime(m.group(1)+'Z', "%Y-%m-%dT%H:%M:%SZ")
|
|
290 |
tm = time.strptime(m.group(1) + 'Z', "%Y-%m-%dT%H:%M:%SZ")
|
|
269 | 291 |
return datetime.datetime.fromtimestamp(time.mktime(tm)) |
270 | 292 | |
271 | 293 |
def authnresponse_checking(login, subject_confirmation, logger, saml_request_id=None): |
... | ... | |
282 | 304 |
try: |
283 | 305 |
irt = assertion.subject. \ |
284 | 306 |
subjectConfirmation.subjectConfirmationData.inResponseTo |
285 |
except: |
|
307 |
except Exception:
|
|
286 | 308 |
pass |
287 | 309 |
logger.debug('inResponseTo: %s' % irt) |
288 | 310 | |
... | ... | |
294 | 316 |
try: |
295 | 317 |
if assertion.subject.subjectConfirmation.method != \ |
296 | 318 |
'urn:oasis:names:tc:SAML:2.0:cm:bearer': |
297 |
logger.error('Unknown \ |
|
298 |
SubjectConfirmation Method') |
|
319 |
logger.error('Unknown SubjectConfirmation Method') |
|
299 | 320 |
return False |
300 |
except: |
|
301 |
logger.error('Error checking \ |
|
302 |
SubjectConfirmation Method') |
|
321 |
except Exception: |
|
322 |
logger.error('Error checking SubjectConfirmation Method') |
|
303 | 323 |
return False |
304 | 324 |
logger.debug('subjectConfirmation method known') |
305 | 325 | |
... | ... | |
308 | 328 |
if assertion.subject. \ |
309 | 329 |
subjectConfirmation.subjectConfirmationData.recipient != \ |
310 | 330 |
subject_confirmation: |
311 |
logger.error('SubjectConfirmation \ |
|
312 |
Recipient Mismatch, %s is not %s' % (assertion.subject. \ |
|
313 |
subjectConfirmation.subjectConfirmationData.recipient, |
|
314 |
subject_confirmation)) |
|
331 |
logger.error('SubjectConfirmation Recipient Mismatch, %s is not %s', |
|
332 |
assertion.subject.subjectConfirmation.subjectConfirmationData.recipient, |
|
333 |
subject_confirmation) |
|
315 | 334 |
return False |
316 |
except: |
|
317 |
logger.error('Error checking \ |
|
318 |
SubjectConfirmation Recipient') |
|
335 |
except Exception: |
|
336 |
logger.error('Error checking SubjectConfirmation Recipient') |
|
319 | 337 |
return False |
320 |
logger.debug('\ |
|
321 |
the url is the same as in the assertion') |
|
338 |
logger.debug('the url is the same as in the assertion') |
|
322 | 339 | |
323 | 340 |
# Check: AudienceRestriction |
324 | 341 |
try: |
... | ... | |
331 | 348 |
if not audience_ok: |
332 | 349 |
logger.error('Incorrect AudienceRestriction') |
333 | 350 |
return False |
334 |
except: |
|
351 |
except Exception:
|
|
335 | 352 |
logger.error('Error checking AudienceRestriction') |
336 | 353 |
return False |
337 | 354 |
logger.debug('audience restriction respected') |
... | ... | |
341 | 358 |
try: |
342 | 359 |
not_before = assertion.subject. \ |
343 | 360 |
subjectConfirmation.subjectConfirmationData.notBefore |
344 |
except: |
|
361 |
except Exception:
|
|
345 | 362 |
logger.error('missing subjectConfirmationData') |
346 | 363 |
return False |
347 | 364 | |
... | ... | |
363 | 380 |
if not_before and now < iso8601_to_datetime(not_before): |
364 | 381 |
logger.error('Assertion received too early') |
365 | 382 |
return False |
366 |
except: |
|
383 |
except Exception:
|
|
367 | 384 |
logger.error('invalid notBefore value ' + not_before) |
368 | 385 |
return False |
369 | 386 |
try: |
370 | 387 |
if not_on_or_after and now > iso8601_to_datetime(not_on_or_after): |
371 | 388 |
logger.error('Assertion expired') |
372 | 389 |
return False |
373 |
except: |
|
390 |
except Exception:
|
|
374 | 391 |
logger.error('invalid notOnOrAfter value') |
375 | 392 |
return False |
376 | 393 | |
377 | 394 |
logger.debug('assertion validity timeslice respected \ |
378 | 395 |
%s <= %s < %s ' % (not_before, str(now), not_on_or_after)) |
379 | ||
380 | 396 |
return True |
381 | 397 | |
398 | ||
382 | 399 |
def get_attributes_from_assertion(assertion, logger): |
383 | 400 |
attributes = dict() |
384 | 401 |
if not assertion: |
... | ... | |
390 | 407 |
nickname = None |
391 | 408 |
try: |
392 | 409 |
name = attribute.name.decode('ascii') |
393 |
except: |
|
410 |
except Exception:
|
|
394 | 411 |
logger.warning('get_attributes_from_assertion: error decoding name of \ |
395 | 412 |
attribute %s' % attribute.dump()) |
396 | 413 |
else: |
... | ... | |
412 | 429 |
for value in values: |
413 | 430 |
content = [any.exportToXml() for any in value.any] |
414 | 431 |
content = ''.join(content) |
415 |
attributes[(name, format)].append(content.\ |
|
416 |
decode('utf8')) |
|
432 |
attributes[(name, format)].append(content.decode('utf8')) |
|
417 | 433 |
except Exception as e: |
418 | 434 |
message = 'get_attributes_from_assertion: value of an \ |
419 | 435 |
attribute failed to decode as ascii: %s due to %s' |
... | ... | |
426 | 442 |
if __name__ == '__main__': |
427 | 443 |
pkey, _ = x509utils.generate_rsa_keypair() |
428 | 444 |
meta = Saml2Metadata('http://example.com/saml', 'http://example.com/saml/prefix/') |
429 |
bindings2 = [ lasso.SAML2_METADATA_BINDING_SOAP, |
|
430 |
lasso.SAML2_METADATA_BINDING_REDIRECT, |
|
431 |
lasso.SAML2_METADATA_BINDING_POST ] |
|
432 |
options = { 'signing_key': pkey } |
|
445 |
bindings2 = [ |
|
446 |
lasso.SAML2_METADATA_BINDING_SOAP, |
|
447 |
lasso.SAML2_METADATA_BINDING_REDIRECT, |
|
448 |
lasso.SAML2_METADATA_BINDING_POST, |
|
449 |
] |
|
450 |
options = {'signing_key': pkey} |
|
433 | 451 |
meta.add_sp_descriptor(( |
434 | 452 |
('SingleLogoutService', |
435 | 453 |
lasso.SAML2_METADATA_BINDING_SOAP, 'logout', 'logoutReturn' ), |
436 | 454 |
('ManageNameIDService', |
437 | 455 |
bindings2, 'manageNameID', 'manageNameIDReturn' ), |
438 | 456 |
('AssertionConsumerService', |
439 |
[ lasso.SAML2_METADATA_BINDING_POST ], 'acs'),),
|
|
457 |
[lasso.SAML2_METADATA_BINDING_POST ], 'acs'),), |
|
440 | 458 |
options) |
441 | 459 |
root = meta.root_element() |
442 | 460 |
print(etree.tostring(root)) |
src/authentic2/saml/shibboleth/afp_parser.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import xml.etree.ElementTree as ET |
4 | 20 | |
5 | 21 |
from authentic2.saml.shibboleth.utils import FancyTreeBuilder |
6 | 22 | |
23 | ||
7 | 24 |
class NS(object): |
8 | 25 |
AFP = 'urn:mace:shibboleth:2.0:afp' |
9 | 26 |
BASIC = 'urn:mace:shibboleth:2.0:afp:mf:basic' |
... | ... | |
26 | 43 |
ATTRIBUTE_ID = 'attributeID' |
27 | 44 |
ID = 'id' |
28 | 45 | |
46 | ||
29 | 47 |
def parse_attribute_filters_file(path): |
30 | 48 |
tree = ET.parse(path, FancyTreeBuilder(target=ET.TreeBuilder())) |
31 | 49 |
root = tree.getroot() |
32 | 50 |
return parse_attribute_filter_et(root) |
33 | 51 | |
52 | ||
34 | 53 |
def fixqname(element, qname): |
35 | 54 |
prefix, local = qname.split(":") |
36 | 55 |
try: |
... | ... | |
38 | 57 |
except KeyError: |
39 | 58 |
raise SyntaxError("unknown namespace prefix (%s)" % prefix) |
40 | 59 | |
60 | ||
41 | 61 |
def parse_attribute_filter_et(root): |
42 | 62 |
assert root.tag == NS.AF_POLICY_GROUP |
43 | 63 |
d = {} |
src/authentic2/saml/shibboleth/utils.py | ||
---|---|---|
1 |
import xml.etree.ElementTree as ET |
|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
2 | 17 |
from xml.etree.ElementTree import XMLTreeBuilder |
3 | 18 | |
4 | 19 | |
... | ... | |
10 | 25 |
self._namespaces = {} |
11 | 26 |
self._parser.StartNamespaceDeclHandler = self._start_ns |
12 | 27 | |
13 | ||
14 | 28 |
def _start(self, *args): |
15 | 29 |
elem = super(FancyTreeBuilder, self)._start(*args) |
16 | 30 |
elem.namespaces = self._namespaces.copy() |
src/authentic2/saml/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from authentic2.decorators import GlobalCache |
2 | 18 | |
3 | 19 |
src/authentic2/saml/x509utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import base64 |
2 | 18 |
import binascii |
3 | 19 |
import tempfile |
... | ... | |
8 | 24 | |
9 | 25 |
_openssl = 'openssl' |
10 | 26 | |
27 | ||
11 | 28 |
def decapsulate_pem_file(file_or_string): |
12 | 29 |
'''Remove PEM header lines''' |
13 | 30 |
if not isinstance(file_or_string, six.string_types): |
... | ... | |
17 | 34 |
i = content.find('--BEGIN') |
18 | 35 |
j = content.find('\n', i) |
19 | 36 |
k = content.find('--END', j) |
20 |
l = content.rfind('\n', 0, k) |
|
21 |
return content[j+1:l] |
|
37 |
l = content.rfind('\n', 0, k) # noqa: E741 |
|
38 |
return content[j + 1:l] |
|
39 | ||
22 | 40 | |
23 | 41 |
def _call_openssl(args): |
24 | 42 |
'''Use subprocees to spawn an openssl process |
... | ... | |
26 | 44 |
Return a tuple made of the return code and the stdout output |
27 | 45 |
''' |
28 | 46 |
try: |
29 |
process = subprocess.Popen(args=[_openssl]+args,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
|
|
47 |
process = subprocess.Popen(args=[_openssl] + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
30 | 48 |
output = process.communicate()[0] |
31 | 49 |
return process.returncode, output |
32 | 50 |
except OSError: |
33 | 51 |
return 1, None |
34 | 52 | |
35 | 53 | |
36 |
def _protect_file(fd,filepath): |
|
54 |
def _protect_file(fd, filepath):
|
|
37 | 55 |
'''Make a file targeted by a file descriptor readable only by the current user |
38 | 56 | |
39 | 57 |
It's needed to be sure nobody can read the private key file we manage. |
40 | 58 |
''' |
41 |
if hasattr(os, 'fchmod'): |
|
42 |
os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR) |
|
43 |
else: # handle python <2.6 |
|
44 |
os.chmod(filepath, stat.S_IRUSR | stat.S_IWUSR) |
|
59 |
os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR) |
|
45 | 60 | |
46 |
def check_key_pair_consistency(publickey=None,privatekey=None): |
|
61 | ||
62 |
def check_key_pair_consistency(publickey=None, privatekey=None): |
|
47 | 63 |
'''Check if two PEM key pair whether they are publickey or certificate, are |
48 | 64 |
well formed and related. |
49 | 65 |
''' |
... | ... | |
53 | 69 |
publickey_file_fd, publickey_fn = tempfile.mkstemp() |
54 | 70 |
_protect_file(privatekey_file_fd, privatekey_fn) |
55 | 71 |
_protect_file(publickey_file_fd, publickey_fn) |
56 |
os.fdopen(privatekey_file_fd,'w').write(privatekey) |
|
57 |
os.fdopen(publickey_file_fd,'w').write(publickey) |
|
72 |
os.fdopen(privatekey_file_fd, 'w').write(privatekey)
|
|
73 |
os.fdopen(publickey_file_fd, 'w').write(publickey)
|
|
58 | 74 |
if 'BEGIN CERTIFICATE' in publickey: |
59 |
rc1, modulus1 = _call_openssl(['x509', '-in', publickey_fn,'-noout','-modulus'])
|
|
75 |
rc1, modulus1 = _call_openssl(['x509', '-in', publickey_fn, '-noout', '-modulus'])
|
|
60 | 76 |
else: |
61 |
rc1, modulus1 = _call_openssl(['rsa', '-pubin', '-in', publickey_fn,'-noout','-modulus'])
|
|
77 |
rc1, modulus1 = _call_openssl(['rsa', '-pubin', '-in', publickey_fn, '-noout', '-modulus'])
|
|
62 | 78 |
if rc1 != 0: |
63 |
rc1, modulus1 = _call_openssl(['dsa', '-pubin', '-in', publickey_fn,'-noout','-modulus'])
|
|
79 |
rc1, modulus1 = _call_openssl(['dsa', '-pubin', '-in', publickey_fn, '-noout', '-modulus'])
|
|
64 | 80 | |
65 | 81 |
if rc1 != 0: |
66 | 82 |
return False |
67 | 83 | |
68 |
rc2, modulus2 = _call_openssl(['rsa', '-in', privatekey_fn,'-noout','-modulus'])
|
|
84 |
rc2, modulus2 = _call_openssl(['rsa', '-in', privatekey_fn, '-noout', '-modulus'])
|
|
69 | 85 |
if rc2 != 0: |
70 |
rc2, modulus2 = _call_openssl(['dsa', '-in', privatekey_fn,'-noout','-modulus'])
|
|
86 |
rc2, modulus2 = _call_openssl(['dsa', '-in', privatekey_fn, '-noout', '-modulus'])
|
|
71 | 87 | |
72 | 88 |
if rc1 == 0 and rc2 == 0 and modulus1 == modulus2: |
73 | 89 |
return True |
... | ... | |
78 | 94 |
os.unlink(publickey_fn) |
79 | 95 |
return None |
80 | 96 | |
97 | ||
81 | 98 |
def generate_rsa_keypair(numbits=1024): |
82 | 99 |
'''Generate simple RSA public and private key files |
83 | 100 |
''' |
... | ... | |
86 | 103 |
publickey_file_fd, publickey_fn = tempfile.mkstemp() |
87 | 104 |
_protect_file(privatekey_file_fd, privatekey_fn) |
88 | 105 |
_protect_file(publickey_file_fd, publickey_fn) |
89 |
rc1, _ = _call_openssl(['genrsa','-out', privatekey_fn,'-passout', 'pass:',str(numbits)])
|
|
90 |
rc2, _ = _call_openssl(['rsa','-in', privatekey_fn,'-pubout','-out', publickey_fn])
|
|
106 |
rc1, _ = _call_openssl(['genrsa', '-out', privatekey_fn, '-passout', 'pass:', str(numbits)])
|
|
107 |
rc2, _ = _call_openssl(['rsa', '-in', privatekey_fn, '-pubout', '-out', publickey_fn])
|
|
91 | 108 |
if rc1 != 0 or rc2 != 0: |
92 | 109 |
raise Exception('Failed to generate a key') |
93 | 110 |
return (os.fdopen(publickey_file_fd).read(), os.fdopen(privatekey_file_fd).read()) |
... | ... | |
95 | 112 |
os.unlink(privatekey_fn) |
96 | 113 |
os.unlink(publickey_fn) |
97 | 114 | |
115 | ||
98 | 116 |
def get_rsa_public_key_modulus(publickey): |
99 | 117 |
try: |
100 | 118 |
publickey_file_fd, publickey_fn = tempfile.mkstemp() |
101 |
os.fdopen(publickey_file_fd,'w').write(publickey) |
|
119 |
os.fdopen(publickey_file_fd, 'w').write(publickey)
|
|
102 | 120 |
if 'BEGIN PUBLIC' in publickey: |
103 |
rc, modulus = _call_openssl(['rsa', '-pubin', '-in', publickey_fn,'-noout','-modulus'])
|
|
121 |
rc, modulus = _call_openssl(['rsa', '-pubin', '-in', publickey_fn, '-noout', '-modulus'])
|
|
104 | 122 |
elif 'BEGIN RSA PRIVATE KEY' in publickey: |
105 | 123 |
rc, modulus = _call_openssl(['rsa', '-in', publickey_fn, '-noout', '-modulus']) |
106 | 124 |
elif 'BEGIN CERTIFICATE' in publickey: |
107 |
rc, modulus = _call_openssl(['x509', '-in', publickey_fn,'-noout','-modulus'])
|
|
125 |
rc, modulus = _call_openssl(['x509', '-in', publickey_fn, '-noout', '-modulus'])
|
|
108 | 126 |
else: |
109 | 127 |
return None |
110 | 128 |
i = modulus.find('=') |
111 | 129 |
if rc == 0 and i: |
112 |
return int(modulus[i+1:].strip(),16)
|
|
130 |
return int(modulus[i + 1:].strip(), 16)
|
|
113 | 131 |
finally: |
114 | 132 |
os.unlink(publickey_fn) |
115 | 133 |
return None |
116 | 134 | |
135 | ||
117 | 136 |
def get_rsa_public_key_exponent(publickey): |
118 | 137 |
try: |
119 | 138 |
publickey_file_fd, publickey_fn = tempfile.mkstemp() |
120 |
os.fdopen(publickey_file_fd,'w').write(publickey) |
|
139 |
os.fdopen(publickey_file_fd, 'w').write(publickey)
|
|
121 | 140 |
_exponent = 'Exponent: ' |
122 | 141 |
if 'BEGIN PUBLIC' in publickey: |
123 |
rc, modulus = _call_openssl(['rsa', '-pubin', '-in', publickey_fn,'-noout','-text'])
|
|
142 |
rc, modulus = _call_openssl(['rsa', '-pubin', '-in', publickey_fn, '-noout', '-text'])
|
|
124 | 143 |
elif 'BEGIN RSA PRIVATE' in publickey: |
125 | 144 |
rc, modulus = _call_openssl(['rsa', '-in', publickey_fn, '-noout', '-text']) |
126 | 145 |
_exponent = 'publicExponent: ' |
127 | 146 |
elif 'BEGIN CERTIFICATE' in publickey: |
128 |
rc, modulus = _call_openssl(['x509', '-in', publickey_fn,'-noout','-text'])
|
|
147 |
rc, modulus = _call_openssl(['x509', '-in', publickey_fn, '-noout', '-text'])
|
|
129 | 148 |
else: |
130 | 149 |
return None |
131 | 150 |
i = modulus.find(_exponent) |
132 | 151 |
j = modulus.find('(', i) |
133 | 152 |
if rc == 0 and i and j: |
134 |
return int(modulus[i+len(_exponent):j].strip())
|
|
153 |
return int(modulus[i + len(_exponent):j].strip())
|
|
135 | 154 |
finally: |
136 | 155 |
os.unlink(publickey_fn) |
137 | 156 |
return None |
138 | 157 | |
158 | ||
139 | 159 |
def can_generate_rsa_key_pair(): |
140 | 160 |
syspath = os.environ.get('PATH') |
141 | 161 |
if syspath: |
142 | 162 |
for base in syspath.split(':'): |
143 |
if os.path.exists(os.path.join(base,'openssl')): |
|
163 |
if os.path.exists(os.path.join(base, 'openssl')):
|
|
144 | 164 |
return True |
145 | 165 |
else: |
146 | 166 |
return False |
147 | 167 | |
168 | ||
148 | 169 |
def get_xmldsig_rsa_key_value(publickey): |
149 | 170 |
def int_to_bin(i): |
150 | 171 |
h = hex(i)[2:].strip('L') |
... | ... | |
154 | 175 | |
155 | 176 |
mod = get_rsa_public_key_modulus(publickey) |
156 | 177 |
exp = get_rsa_public_key_exponent(publickey) |
157 |
return '<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">\n\t<Modulus>%s</Modulus>\n\t<Exponent>%s</Exponent>\n</RSAKeyValue>' % (base64.b64encode(int_to_bin(mod)), base64.b64encode(int_to_bin(exp))) |
|
178 |
return ( |
|
179 |
'<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">\n\t' |
|
180 |
'<Modulus>%s</Modulus>\n\t' |
|
181 |
'<Exponent>%s</Exponent>\n</RSAKeyValue>' % ( |
|
182 |
base64.b64encode(int_to_bin(mod)), base64.b64encode(int_to_bin(exp)))) |
|
158 | 183 | |
159 | 184 | |
160 | 185 |
if __name__ == '__main__': |
... | ... | |
200 | 225 |
-----END RSA PRIVATE KEY-----''' |
201 | 226 |
assert(check_key_pair_consistency(cert, key)) |
202 | 227 |
assert(get_xmldsig_rsa_key_value(cert)) |
203 |
assert(len(decapsulate_pem_file(key).splitlines()) == len(key.splitlines())-2)
|
|
228 |
assert(len(decapsulate_pem_file(key).splitlines()) == len(key.splitlines()) - 2)
|
|
204 | 229 |
src/authentic2/serializers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import json |
2 | 18 |
import sys |
3 | 19 | |
... | ... | |
31 | 47 |
self._current[vfield.name] = (ct.natural_key(), sub_obj.natural_key()) |
32 | 48 |
super(Serializer, self).end_object(obj) |
33 | 49 | |
50 | ||
34 | 51 |
def PreDeserializer(objects, **options): |
35 | 52 |
db = options.pop('using', DEFAULT_DB_ALIAS) |
36 | 53 | |
... | ... | |
39 | 56 |
for vfield in Model._meta.virtual_fields: |
40 | 57 |
if not isinstance(vfield, GenericForeignKey): |
41 | 58 |
continue |
42 |
if not vfield.name in d['fields']:
|
|
59 |
if vfield.name not in d['fields']:
|
|
43 | 60 |
continue |
44 | 61 |
ct_natural_key, fk_natural_key = d['fields'][vfield.name] |
45 | 62 |
ct = ContentType.objects.get_by_natural_key(*ct_natural_key) |
... | ... | |
49 | 66 |
del d['fields'][vfield.name] |
50 | 67 |
yield d |
51 | 68 | |
69 | ||
52 | 70 |
def Deserializer(stream_or_string, **options): |
53 | 71 |
""" |
54 | 72 |
Deserialize a stream or string of JSON data. |
src/authentic2/settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
import logging.config |
3 | 19 |
# Load default from Django |
... | ... | |
12 | 28 |
CACHES = global_settings.CACHES |
13 | 29 | |
14 | 30 |
BASE_DIR = os.path.dirname(__file__) |
15 |
### Quick-start development settings - unsuitable for production |
|
31 | ||
32 |
# Quick-start development settings - unsuitable for production |
|
16 | 33 |
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ |
17 | 34 | |
18 | 35 |
# SECURITY WARNING: keep the secret key used in production secret! |
... | ... | |
36 | 53 |
} |
37 | 54 |
} |
38 | 55 | |
39 |
### End of "Quick-start development settings" |
|
40 | ||
41 | ||
42 | 56 |
# Hey Entr'ouvert is in France !! |
43 | 57 |
TIME_ZONE = 'Europe/Paris' |
44 | 58 |
LANGUAGE_CODE = 'fr' |
... | ... | |
91 | 105 | |
92 | 106 |
MIDDLEWARE_CLASSES += ( |
93 | 107 |
'authentic2.middleware.DisplayMessageBeforeRedirectMiddleware', |
94 |
'authentic2.idp.middleware.DebugMiddleware', |
|
95 | 108 |
'authentic2.middleware.CollectIPMiddleware', |
96 | 109 |
'authentic2.middleware.ViewRestrictionMiddleware', |
97 | 110 |
'authentic2.middleware.OpenedSessionCookieMiddleware', |
... | ... | |
103 | 116 | |
104 | 117 |
STATICFILES_FINDERS = list(global_settings.STATICFILES_FINDERS) + ['gadjo.finders.XStaticFinder'] |
105 | 118 | |
106 |
LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), )
|
|
119 |
LOCALE_PATHS = (os.path.join(BASE_DIR, 'locale'), ) |
|
107 | 120 | |
108 | 121 |
INSTALLED_APPS = ( |
109 | 122 |
'django.contrib.staticfiles', |
... | ... | |
144 | 157 |
'authentic2.backends.models_backend.DummyModelBackend', |
145 | 158 |
'django_rbac.backends.DjangoRBACBackend', |
146 | 159 |
) |
147 |
AUTHENTICATION_BACKENDS = plugins.register_plugins_authentication_backends( |
|
148 |
AUTHENTICATION_BACKENDS) |
|
160 |
AUTHENTICATION_BACKENDS = plugins.register_plugins_authentication_backends(AUTHENTICATION_BACKENDS) |
|
149 | 161 |
CSRF_FAILURE_VIEW = 'authentic2.views.csrf_failure_view' |
150 | 162 | |
151 | 163 | |
... | ... | |
186 | 198 |
# Can be none, sp, idp or both |
187 | 199 | |
188 | 200 |
PASSWORD_HASHERS = list(global_settings.PASSWORD_HASHERS) + [ |
189 |
'authentic2.hashers.Drupal7PasswordHasher',
|
|
190 |
'authentic2.hashers.SHA256PasswordHasher',
|
|
191 |
'authentic2.hashers.SSHA1PasswordHasher',
|
|
192 |
'authentic2.hashers.SMD5PasswordHasher',
|
|
193 |
'authentic2.hashers.SHA1OLDAPPasswordHasher',
|
|
194 |
'authentic2.hashers.MD5OLDAPPasswordHasher',
|
|
195 |
'authentic2.hashers.PloneSHA1PasswordHasher',
|
|
201 |
'authentic2.hashers.Drupal7PasswordHasher', |
|
202 |
'authentic2.hashers.SHA256PasswordHasher', |
|
203 |
'authentic2.hashers.SSHA1PasswordHasher', |
|
204 |
'authentic2.hashers.SMD5PasswordHasher', |
|
205 |
'authentic2.hashers.SHA1OLDAPPasswordHasher', |
|
206 |
'authentic2.hashers.MD5OLDAPPasswordHasher', |
|
207 |
'authentic2.hashers.PloneSHA1PasswordHasher', |
|
196 | 208 |
] |
197 | 209 | |
198 | 210 |
# Admin tools |
... | ... | |
202 | 214 | |
203 | 215 |
# Serialization module to support natural keys in generic foreign keys |
204 | 216 |
SERIALIZATION_MODULES = { |
205 |
'json': 'authentic2.serializers',
|
|
217 |
'json': 'authentic2.serializers', |
|
206 | 218 |
} |
207 | 219 | |
208 | 220 |
LOGGING_CONFIG = None |
... | ... | |
211 | 223 |
'disable_existing_loggers': True, |
212 | 224 |
'filters': { |
213 | 225 |
'cleaning': { |
214 |
'()': 'authentic2.utils.CleanLogMessage',
|
|
226 |
'()': 'authentic2.utils.CleanLogMessage', |
|
215 | 227 |
}, |
216 | 228 |
'request_context': { |
217 |
'()': 'authentic2.log_filters.RequestContextFilter',
|
|
229 |
'()': 'authentic2.log_filters.RequestContextFilter', |
|
218 | 230 |
}, |
219 | 231 |
'force_debug': { |
220 | 232 |
'()': 'authentic2.log_filters.ForceDebugFilter', |
... | ... | |
233 | 245 |
'handlers': { |
234 | 246 |
'console': { |
235 | 247 |
'level': 'DEBUG', |
236 |
'class':'logging.StreamHandler', |
|
248 |
'class': 'logging.StreamHandler',
|
|
237 | 249 |
'formatter': 'verbose', |
238 | 250 |
'filters': ['cleaning', 'request_context'], |
239 | 251 |
}, |
240 |
# remove request_context filter for db log to prevent infinite loop
|
|
241 |
# when logging sql query to retrieve the session user
|
|
252 |
# remove request_context filter for db log to prevent infinite loop
|
|
253 |
# when logging sql query to retrieve the session user
|
|
242 | 254 |
'console_db': { |
243 | 255 |
'level': 'DEBUG', |
244 |
'class':'logging.StreamHandler', |
|
256 |
'class': 'logging.StreamHandler',
|
|
245 | 257 |
'formatter': 'verbose_db', |
246 | 258 |
'filters': ['cleaning'], |
247 | 259 |
}, |
... | ... | |
250 | 262 |
# even when debugging seeing SQL queries is too much, activate it |
251 | 263 |
# explicitly using DEBUG_DB |
252 | 264 |
'django.db': { |
253 |
'handlers': ['console_db'],
|
|
254 |
'level': logger.SettingsLogLevel('INFO', debug_setting='DEBUG_DB'),
|
|
255 |
'propagate': False,
|
|
265 |
'handlers': ['console_db'], |
|
266 |
'level': logger.SettingsLogLevel('INFO', debug_setting='DEBUG_DB'), |
|
267 |
'propagate': False, |
|
256 | 268 |
}, |
257 | 269 |
'django': { |
258 |
'level': 'INFO',
|
|
270 |
'level': 'INFO', |
|
259 | 271 |
}, |
260 | 272 |
# django_select2 outputs debug message at level INFO |
261 | 273 |
'django_select2': { |
262 |
'level': 'WARNING',
|
|
274 |
'level': 'WARNING', |
|
263 | 275 |
}, |
264 | 276 |
# lasso has the bad habit of logging everything as errors |
265 | 277 |
'Lasso': { |
... | ... | |
272 | 284 |
'filters': ['force_debug'], |
273 | 285 |
}, |
274 | 286 |
'': { |
275 |
'handlers': ['console'],
|
|
276 |
'level': logger.SettingsLogLevel('INFO'),
|
|
287 |
'handlers': ['console'], |
|
288 |
'level': logger.SettingsLogLevel('INFO'), |
|
277 | 289 |
}, |
278 | 290 |
}, |
279 | 291 |
} |
280 | 292 | |
281 | 293 |
MIGRATION_MODULES = { |
282 |
'auth': 'authentic2.auth_migrations',
|
|
283 |
'menu': 'authentic2.menu_migrations',
|
|
284 |
'dashboard': 'authentic2.dashboard_migrations',
|
|
294 |
'auth': 'authentic2.auth_migrations', |
|
295 |
'menu': 'authentic2.menu_migrations', |
|
296 |
'dashboard': 'authentic2.dashboard_migrations', |
|
285 | 297 |
} |
286 | 298 |
MIGRATION_MODULES['auth'] = 'authentic2.auth_migrations_18' |
287 | 299 |
src/authentic2/templates/registration/registration_completion_choose.html | ||
---|---|---|
1 | 1 |
{% extends "authentic2/base-page.html" %} |
2 | 2 |
{% load i18n %} |
3 |
{% load breadcrumbs %} |
|
4 | 3 | |
5 | 4 |
{% block title %} |
6 | 5 |
{% trans "Registration" %} |
7 | 6 |
{% endblock %} |
8 | 7 | |
9 |
{% block breadcrumbs %} |
|
10 |
{{ block.super }} |
|
11 |
{% breadcrumb_url 'Register' %} |
|
12 |
{% endblock %} |
|
13 | ||
14 | 8 |
{% block content %} |
15 | 9 |
<h2>{% trans "Login" %}</h2> |
16 | 10 |
<p> |
src/authentic2/templates/registration/registration_completion_form.html | ||
---|---|---|
1 | 1 |
{% extends "authentic2/base-page.html" %} |
2 | 2 |
{% load i18n %} |
3 |
{% load breadcrumbs %} |
|
4 | 3 | |
5 | 4 |
{% block title %} |
6 | 5 |
{% trans "Registration" %} |
7 | 6 |
{% endblock %} |
8 | 7 | |
9 |
{% block breadcrumbs %} |
|
10 |
{{ block.super }} |
|
11 |
{% breadcrumb_url 'Register' %} |
|
12 |
{% endblock %} |
|
13 | ||
14 | 8 |
{% block content %} |
15 | 9 |
<h2>{% trans "Registration" %}</h2> |
16 | 10 |
<p>{% trans "Please fill the form to complete your registration" %}</p> |
src/authentic2/templates/registration/registration_form.html | ||
---|---|---|
5 | 5 |
{{ view.title }} |
6 | 6 |
{% endblock %} |
7 | 7 | |
8 |
{% load breadcrumbs %} |
|
9 |
{% block breadcrumbs %} |
|
10 |
{{ block.super }} |
|
11 |
{% breadcrumb_url 'Register' %} |
|
12 |
{% endblock %} |
|
13 | ||
14 | 8 |
{% block content %} |
15 | 9 | |
16 | 10 |
<h2>{{ view.title }}</h2> |
src/authentic2/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url, include |
2 | 18 |
from django.conf import settings |
3 | 19 |
from django.contrib import admin |
20 |
from django.contrib.auth.decorators import login_required |
|
21 |
from django.contrib.auth import views as dj_auth_views |
|
4 | 22 |
from django.contrib.staticfiles.views import serve |
23 |
from django.views.generic.base import TemplateView |
|
5 | 24 |
from django.views.static import serve as media_serve |
6 | 25 | |
7 |
from . import app_settings, plugins, views
|
|
26 |
from . import plugins, views |
|
8 | 27 | |
9 | 28 |
admin.autodiscover() |
10 | 29 | |
11 | 30 |
handler500 = 'authentic2.views.server_error' |
12 | 31 | |
13 |
urlpatterns = [ |
|
14 |
url(r'^$', views.homepage, name='auth_homepage'), |
|
15 |
url(r'test_redirect/$', views.test_redirect) |
|
32 |
accounts_urlpatterns = [ |
|
33 |
url(r'^activate/(?P<registration_token>[\w: -]+)/$', |
|
34 |
views.registration_completion, name='registration_activate'), |
|
35 |
url(r'^register/$', |
|
36 |
views.RegistrationView.as_view(), |
|
37 |
name='registration_register'), |
|
38 |
url(r'^register/complete/$', |
|
39 |
views.registration_complete, |
|
40 |
name='registration_complete'), |
|
41 |
url(r'^register/closed/$', |
|
42 |
TemplateView.as_view(template_name='registration/registration_closed.html'), |
|
43 |
name='registration_disallowed'), |
|
44 |
url(r'^delete/$', |
|
45 |
login_required(views.DeleteView.as_view()), |
|
46 |
name='delete_account'), |
|
47 |
url(r'^logged-in/$', |
|
48 |
views.logged_in, |
|
49 |
name='logged-in'), |
|
50 |
url(r'^edit/$', |
|
51 |
views.edit_profile, |
|
52 |
name='profile_edit'), |
|
53 |
url(r'^edit/(?P<scope>[-\w]+)/$', |
|
54 |
views.edit_profile, |
|
55 |
name='profile_edit_with_scope'), |
|
56 |
url(r'^change-email/$', |
|
57 |
views.email_change, |
|
58 |
name='email-change'), |
|
59 |
url(r'^change-email/verify/$', |
|
60 |
views.email_change_verify, |
|
61 |
name='email-change-verify'), |
|
62 |
url(r'^$', |
|
63 |
views.profile, |
|
64 |
name='account_management'), |
|
65 | ||
66 |
# Password change |
|
67 |
url(r'^password/change/$', |
|
68 |
views.password_change, |
|
69 |
name='password_change'), |
|
70 |
url(r'^password/change/done/$', |
|
71 |
dj_auth_views.password_change_done, |
|
72 |
name='password_change_done'), |
|
73 | ||
74 |
# Password reset |
|
75 |
url(r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
|
76 |
views.password_reset_confirm, |
|
77 |
name='password_reset_confirm'), |
|
78 |
url(r'^password/reset/$', |
|
79 |
views.password_reset, |
|
80 |
name='password_reset'), |
|
81 | ||
82 |
url(r'^switch-back/$', |
|
83 |
views.switch_back, |
|
84 |
name='a2-switch-back'), |
|
85 | ||
86 |
# Legacy, only there to provide old view names to resolver |
|
87 |
url(r'^password/change/$', |
|
88 |
views.notimplemented_view, |
|
89 |
name='auth_password_change'), |
|
90 |
url(r'^password/change/done/$', |
|
91 |
views.notimplemented_view, |
|
92 |
name='auth_password_change_done'), |
|
93 | ||
94 |
url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
|
95 |
views.notimplemented_view, |
|
96 |
name='auth_password_reset_confirm'), |
|
97 |
url(r'^password/reset/$', |
|
98 |
views.notimplemented_view, |
|
99 |
name='auth_password_reset'), |
|
100 |
url(r'^password/reset/complete/$', |
|
101 |
views.notimplemented_view, |
|
102 |
name='auth_password_reset_complete'), |
|
103 |
url(r'^password/reset/done/$', |
|
104 |
views.notimplemented_view, |
|
105 |
name='auth_password_reset_done'), |
|
16 | 106 |
] |
17 | 107 | |
18 |
not_homepage_patterns = [ |
|
108 |
urlpatterns = [ |
|
109 |
url(r'^$', views.homepage, name='auth_homepage'), |
|
19 | 110 |
url(r'^login/$', views.login, name='auth_login'), |
20 | 111 |
url(r'^logout/$', views.logout, name='auth_logout'), |
21 |
url(r'^redirect/(.*)', views.redirect, name='auth_redirect'), |
|
22 |
url(r'^accounts/', include('authentic2.profile_urls')) |
|
23 |
] |
|
24 | ||
25 |
not_homepage_patterns += [ |
|
26 |
url(r'^accounts/', include(app_settings.A2_REGISTRATION_URLCONF)), |
|
112 |
url(r'^accounts/', include(accounts_urlpatterns)), |
|
27 | 113 |
url(r'^admin/', include(admin.site.urls)), |
28 | 114 |
url(r'^idp/', include('authentic2.idp.urls')), |
29 | 115 |
url(r'^manage/', include('authentic2.manager.urls')), |
30 |
url(r'^api/', include('authentic2.api_urls')) |
|
116 |
url(r'^api/', include('authentic2.api_urls')),
|
|
31 | 117 |
] |
32 | 118 | |
33 | ||
34 |
urlpatterns += not_homepage_patterns |
|
35 | ||
36 | 119 |
try: |
37 | 120 |
if getattr(settings, 'DISCO_SERVICE', False): |
38 | 121 |
urlpatterns += [ |
39 | 122 |
(r'^disco_service/', include('disco_service.disco_responder')), |
40 | 123 |
] |
41 |
except: |
|
124 |
except Exception:
|
|
42 | 125 |
pass |
43 | 126 | |
44 | 127 |
if settings.DEBUG: |
... | ... | |
46 | 129 |
url(r'^static/(?P<path>.*)$', serve) |
47 | 130 |
] |
48 | 131 |
urlpatterns += [ |
49 |
url(r'^media/(?P<path>.*)$', media_serve, { |
|
50 |
'document_root': settings.MEDIA_ROOT}) |
|
132 |
url(r'^media/(?P<path>.*)$', media_serve, |
|
133 |
{ |
|
134 |
'document_root': settings.MEDIA_ROOT |
|
135 |
}) |
|
51 | 136 |
] |
52 | 137 | |
53 | 138 |
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: |
src/authentic2/user_login_failure.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
import hashlib |
3 | 19 | |
... | ... | |
6 | 22 | |
7 | 23 |
from . import app_settings |
8 | 24 | |
25 | ||
9 | 26 |
def key(identifier): |
10 | 27 |
return 'user-login-failure-%s' % hashlib.md5(smart_bytes(identifier)).hexdigest() |
11 | 28 | |
29 | ||
12 | 30 |
def user_login_success(identifier): |
13 | 31 |
cache.delete(key(identifier)) |
14 | 32 | |
33 | ||
15 | 34 |
def user_login_failure(identifier): |
16 | 35 |
cache.add(key(identifier), 0) |
17 | 36 |
count = cache.incr(key(identifier)) |
18 | 37 |
logger = logging.getLogger('authentic2.user_login_failure') |
19 | 38 |
logger.info(u'user %s failed to login', identifier) |
20 |
if app_settings.A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING and count >= app_settings.A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING:
|
|
21 |
logger.warning(u'user %s failed to login more than %d times in a row',
|
|
22 |
identifier, count)
|
|
39 |
if (app_settings.A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING
|
|
40 |
and count >= app_settings.A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING):
|
|
41 |
logger.warning(u'user %s failed to login more than %d times in a row', identifier, count)
|
|
23 | 42 |
src/authentic2/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import inspect |
2 | 18 |
import random |
3 | 19 |
import time |
... | ... | |
21 | 37 |
from django import forms |
22 | 38 |
from django.forms.utils import ErrorList, to_current_timezone |
23 | 39 |
from django.utils import timezone |
24 |
from django.utils import html, http, six, encoding
|
|
40 |
from django.utils import html, six, encoding |
|
25 | 41 |
from django.utils.translation import ugettext as _, ungettext |
26 | 42 |
from django.utils.six.moves.urllib import parse as urlparse |
27 | 43 |
from django.shortcuts import resolve_url |
28 | 44 |
from django.template.loader import render_to_string, TemplateDoesNotExist |
29 | 45 |
from django.core.mail import send_mail |
30 | 46 |
from django.core import signing |
31 |
from django.core.urlresolvers import reverse, NoReverseMatch
|
|
47 |
from django.core.urlresolvers import reverse |
|
32 | 48 |
from django.utils.formats import localize |
33 | 49 |
from django.contrib import messages |
34 |
from django.utils import six |
|
35 | 50 |
from django.utils.functional import empty, allow_lazy |
36 | 51 |
from django.utils.http import urlsafe_base64_encode |
37 | 52 |
from django.utils.encoding import iri_to_uri, force_bytes, uri_to_iri |
38 |
from django.utils import six |
|
39 | 53 |
from django.shortcuts import render |
40 | 54 | |
41 | 55 | |
... | ... | |
141 | 155 |
mod = import_module(module) |
142 | 156 |
except ImportError as e: |
143 | 157 |
raise ImproperlyConfigured('Error importing idp backend %s: "%s"' % (module, e)) |
144 |
except ValueError as e:
|
|
158 |
except ValueError: |
|
145 | 159 |
raise ImproperlyConfigured('Error importing idp backends. Is IDP_BACKENDS a correctly ' |
146 | 160 |
'defined list or tuple?') |
147 | 161 |
try: |
... | ... | |
200 | 214 |
content = response.content |
201 | 215 |
status_code = response.status_code |
202 | 216 |
return { |
203 |
'id': authenticator.id,
|
|
204 |
'name': authenticator.name,
|
|
205 |
'content': content,
|
|
206 |
'response': response,
|
|
207 |
'status_code': status_code,
|
|
208 |
'authenticator': authenticator,
|
|
217 |
'id': authenticator.id, |
|
218 |
'name': authenticator.name, |
|
219 |
'content': content, |
|
220 |
'response': response, |
|
221 |
'status_code': status_code, |
|
222 |
'authenticator': authenticator, |
|
209 | 223 |
} |
210 | 224 | |
211 | 225 | |
... | ... | |
252 | 266 |
parsed = urlparse.urlparse(url) |
253 | 267 |
if parsed.scheme in ('http', 'https', ''): |
254 | 268 |
return True |
255 |
except: |
|
269 |
except Exception:
|
|
256 | 270 |
return False |
257 | 271 | |
258 | 272 | |
... | ... | |
445 | 459 |
(6, 'ABCDEFGHJKLMNPQRSTUVWXYZ'), |
446 | 460 |
(1, '%$/\\#@!')) |
447 | 461 |
parts = [] |
448 |
for count, alphabet in composition:
|
|
449 |
for i in range(count):
|
|
462 |
for cnt, alphabet in composition: |
|
463 |
for i in range(cnt): |
|
450 | 464 |
parts.append(random.SystemRandom().choice(alphabet)) |
451 | 465 |
random.shuffle(parts, random.SystemRandom().random) |
452 | 466 |
return ''.join(parts) |
... | ... | |
577 | 591 |
if isinstance(field, (list, tuple)): |
578 | 592 |
field, label = field |
579 | 593 |
labels[field] = label |
580 |
if not field in fields:
|
|
594 |
if field not in fields:
|
|
581 | 595 |
fields.append(field) |
582 | 596 |
return fields, labels |
583 | 597 | |
... | ... | |
906 | 920 | |
907 | 921 |
def select_next_url(request, default, field_name=None, include_post=False, replace=None): |
908 | 922 |
'''Select the first valid next URL''' |
909 |
next_url = (include_post and get_next_url(request.POST, field_name=field_name)) or get_next_url(request.GET, field_name=field_name) |
|
923 |
next_url = ( |
|
924 |
(include_post and get_next_url(request.POST, field_name=field_name)) |
|
925 |
or get_next_url(request.GET, field_name=field_name) |
|
926 |
) |
|
910 | 927 |
if good_next_url(request, next_url): |
911 | 928 |
if replace: |
912 | 929 |
for key, value in replace.items(): |
src/authentic2/validators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import unicode_literals |
2 | 18 | |
3 | 19 |
import smtplib |
... | ... | |
13 | 29 | |
14 | 30 |
from . import app_settings |
15 | 31 |
# keep those symbols here for retrocompatibility |
16 |
from .passwords import password_help_text, validate_password |
|
32 |
from .passwords import password_help_text, validate_password # noqa: F401
|
|
17 | 33 | |
18 | 34 | |
19 | 35 |
# copied from http://www.djangotips.com/real-email-validation |
src/authentic2/views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import collections |
|
1 | 18 |
import logging |
2 |
from authentic2.compat_lasso import lasso |
|
3 |
import requests |
|
19 |
import random |
|
4 | 20 |
import re |
5 |
import collections |
|
6 | ||
7 | 21 | |
8 | 22 |
from django.conf import settings |
9 |
from django.shortcuts import render_to_response, render
|
|
10 |
from django.template.loader import render_to_string, select_template
|
|
23 |
from django.shortcuts import render, get_object_or_404
|
|
24 |
from django.template.loader import render_to_string |
|
11 | 25 |
from django.views.generic.edit import UpdateView, FormView |
12 |
from django.views.generic import RedirectView, TemplateView
|
|
26 |
from django.views.generic import TemplateView |
|
13 | 27 |
from django.views.generic.base import View |
14 | 28 |
from django.contrib.auth import SESSION_KEY |
15 | 29 |
from django import http, shortcuts |
16 |
from django.core import mail, signing
|
|
30 |
from django.core import signing |
|
17 | 31 |
from django.core.urlresolvers import reverse |
18 | 32 |
from django.core.exceptions import ValidationError |
19 | 33 |
from django.contrib import messages |
... | ... | |
21 | 35 |
from django.utils.translation import ugettext as _ |
22 | 36 |
from django.contrib.auth import logout as auth_logout |
23 | 37 |
from django.contrib.auth import REDIRECT_FIELD_NAME |
24 |
from django.http import (HttpResponseRedirect, HttpResponseForbidden, |
|
25 |
HttpResponse) |
|
26 |
from django.core.exceptions import PermissionDenied |
|
38 |
from django.contrib.auth.views import password_change as dj_password_change |
|
39 |
from django.http import (HttpResponseRedirect, HttpResponseForbidden, HttpResponse) |
|
27 | 40 |
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie |
28 | 41 |
from django.views.decorators.cache import never_cache |
42 |
from django.views.decorators.debug import sensitive_post_parameters |
|
29 | 43 |
from django.contrib.auth.decorators import login_required |
30 | 44 |
from django.db.models.fields import FieldDoesNotExist |
31 | 45 |
from django.db.models.query import Q |
32 | ||
33 |
# FIXME: this decorator has nothing to do with an idp, should be moved in the |
|
34 |
# a2 package |
|
35 |
# FIXME: this constant should be moved in the a2 package |
|
36 | ||
37 | ||
38 |
from . import (utils, app_settings, forms, compat, decorators, constants, models, cbv, hooks) |
|
39 | ||
46 |
from django.contrib.auth import get_user_model, authenticate |
|
47 |
from django.http import Http404 |
|
48 |
from django.utils.http import urlsafe_base64_decode |
|
49 |
from django.views.generic.edit import CreateView |
|
50 |
from django.forms import CharField, Form |
|
51 |
from django.core.urlresolvers import reverse_lazy |
|
52 |
from django.http import HttpResponseBadRequest |
|
53 | ||
54 |
from . import (utils, app_settings, compat, decorators, constants, |
|
55 |
models, cbv, hooks, validators) |
|
56 |
from .a2_rbac.utils import get_default_ou |
|
57 |
from .a2_rbac.models import OrganizationalUnit as OU |
|
58 |
from .forms import ( |
|
59 |
passwords as passwords_forms, |
|
60 |
registration as registration_forms, |
|
61 |
profile as profile_forms) |
|
62 | ||
63 |
User = get_user_model() |
|
40 | 64 | |
41 | 65 |
logger = logging.getLogger(__name__) |
42 | 66 | |
43 | 67 | |
44 |
def redirect(request, next, template_name='redirect.html'): |
|
45 |
'''Show a simple page which does a javascript redirect, closing any popup |
|
46 |
enclosing us''' |
|
47 |
if not next.startswith('http'): |
|
48 |
next = '/%s%s' % (request.get_host(), next) |
|
49 |
logging.info('Redirect to %r' % next) |
|
50 |
return render_to_response(template_name, { 'next': next }) |
|
51 | ||
52 | ||
53 | 68 |
def server_error(request, template_name='500.html'): |
54 | 69 |
""" |
55 | 70 |
500 error handler. |
... | ... | |
61 | 76 | |
62 | 77 | |
63 | 78 |
class EditProfile(cbv.HookMixin, cbv.TemplateNamesMixin, UpdateView): |
64 |
model = compat.get_user_model()
|
|
79 |
model = User
|
|
65 | 80 |
template_names = ['profiles/edit_profile.html', |
66 | 81 |
'authentic2/accounts_edit.html'] |
67 | 82 |
title = _('Edit account data') |
... | ... | |
100 | 115 |
else: |
101 | 116 |
default_fields = list(attributes.values_list('name', flat=True)) |
102 | 117 |
fields, labels = utils.get_fields_and_labels( |
103 |
editable_profile_fields, |
|
104 |
default_fields) |
|
118 |
editable_profile_fields, default_fields) |
|
105 | 119 |
if scopes: |
106 | 120 |
# restrict fields to those in the scopes |
107 | 121 |
fields = [field for field in fields if field in default_fields] |
... | ... | |
115 | 129 |
fields, labels = self.get_fields(scopes=scopes) |
116 | 130 |
# Email must be edited through the change email view, as it needs validation |
117 | 131 |
fields = [field for field in fields if field != 'email'] |
118 |
return forms.modelform_factory(compat.get_user_model(), fields=fields, |
|
119 |
labels=labels, |
|
120 |
form=forms.EditProfileForm) |
|
132 |
return profile_forms.modelform_factory( |
|
133 |
User, fields=fields, |
|
134 |
labels=labels, |
|
135 |
form=profile_forms.EditProfileForm) |
|
121 | 136 | |
122 | 137 |
def get_object(self): |
123 | 138 |
return self.request.user |
... | ... | |
154 | 169 |
url(r'^su/(?P<username>.*)/$', 'authentic2.views.su', {'redirect_url': '/'}), |
155 | 170 |
''' |
156 | 171 |
if request.user.is_superuser or request.session.get('has_superuser_power'): |
157 |
su_user = shortcuts.get_object_or_404(compat.get_user_model(), username=username)
|
|
172 |
su_user = shortcuts.get_object_or_404(User, username=username)
|
|
158 | 173 |
if su_user.is_active: |
159 | 174 |
request.session[SESSION_KEY] = su_user.id |
160 | 175 |
request.session['has_superuser_power'] = True |
... | ... | |
173 | 188 | |
174 | 189 |
def get_form_class(self): |
175 | 190 |
if self.request.user.has_usable_password(): |
176 |
return forms.EmailChangeForm |
|
177 |
return forms.EmailChangeFormNoPassword |
|
191 |
return profile_forms.EmailChangeForm
|
|
192 |
return profile_forms.EmailChangeFormNoPassword
|
|
178 | 193 | |
179 | 194 |
def get_form_kwargs(self): |
180 | 195 |
kwargs = super(EmailChangeView, self).get_form_kwargs() |
... | ... | |
196 | 211 |
'is received. An email of validation ' |
197 | 212 |
'was sent to you. Please click on the ' |
198 | 213 |
'link contained inside.')) |
199 |
logging.getLogger(__name__).info('email change request')
|
|
214 |
logger.info('email change request')
|
|
200 | 215 |
return super(EmailChangeView, self).form_valid(form) |
201 | 216 | |
202 | 217 |
email_change = decorators.setting_enabled('A2_PROFILE_CAN_CHANGE_EMAIL')( |
... | ... | |
206 | 221 |
class EmailChangeVerifyView(TemplateView): |
207 | 222 |
def get(self, request, *args, **kwargs): |
208 | 223 |
if 'token' in request.GET: |
209 |
User = compat.get_user_model() |
|
210 | 224 |
try: |
211 | 225 |
token = signing.loads(request.GET['token'], |
212 | 226 |
max_age=app_settings.A2_EMAIL_CHANGE_TOKEN_LIFETIME) |
... | ... | |
225 | 239 |
user.email = email |
226 | 240 |
user.email_verified = True |
227 | 241 |
user.save() |
228 |
messages.info(request, _('your request for changing your email for {0} ' |
|
229 |
'is successful').format(email)) |
|
230 |
logging.getLogger(__name__).info('user %s changed its email ' |
|
231 |
'from %s to %s', user, |
|
232 |
old_email, email) |
|
242 |
messages.info(request, |
|
243 |
_('your request for changing your email for {0} is successful').format(email)) |
|
244 |
logger.info('user %s changed its email from %s to %s', user, old_email, email) |
|
233 | 245 |
hooks.call_hooks('event', name='change-email-confirm', user=user, email=email) |
234 | 246 |
except signing.SignatureExpired: |
235 |
messages.error(request, _('your request for changing your email is too '
|
|
236 |
'old, try again'))
|
|
247 |
messages.error(request, |
|
248 |
_('your request for changing your email is too old, try again'))
|
|
237 | 249 |
except signing.BadSignature: |
238 |
messages.error(request, _('your request for changing your email is '
|
|
239 |
'invalid, try again'))
|
|
250 |
messages.error(request, |
|
251 |
_('your request for changing your email is invalid, try again'))
|
|
240 | 252 |
except ValueError: |
241 |
messages.error(request, _('your request for changing your email was not '
|
|
242 |
'on this site, try again'))
|
|
253 |
messages.error(request, |
|
254 |
_('your request for changing your email was not on this site, try again'))
|
|
243 | 255 |
except User.DoesNotExist: |
244 |
messages.error(request, _('your request for changing your email is for '
|
|
245 |
'an unknown user, try again'))
|
|
256 |
messages.error(request, |
|
257 |
_('your request for changing your email is for an unknown user, try again'))
|
|
246 | 258 |
except ValidationError as e: |
247 | 259 |
messages.error(request, e.message) |
248 | 260 |
else: |
... | ... | |
252 | 264 | |
253 | 265 |
email_change_verify = EmailChangeVerifyView.as_view() |
254 | 266 | |
255 |
logger = logging.getLogger('authentic2.idp.views') |
|
256 | ||
257 | 267 | |
258 | 268 |
@csrf_exempt |
259 | 269 |
@ensure_csrf_cookie |
... | ... | |
264 | 274 | |
265 | 275 |
# redirect user to homepage if already connected, if setting |
266 | 276 |
# A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE is True |
267 |
if (request.user.is_authenticated() and
|
|
268 |
app_settings.A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE): |
|
277 |
if (request.user.is_authenticated() |
|
278 |
and app_settings.A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE):
|
|
269 | 279 |
return utils.redirect(request, 'auth_homepage') |
270 | 280 | |
271 | 281 |
redirect_to = request.GET.get(redirect_field_name) |
... | ... | |
308 | 318 |
form_class = authenticator.form() |
309 | 319 |
submit_name = 'submit-%s' % fid |
310 | 320 |
block = { |
311 |
'id': fid,
|
|
312 |
'name': name,
|
|
313 |
'authenticator': authenticator
|
|
321 |
'id': fid, |
|
322 |
'name': name, |
|
323 |
'authenticator': authenticator |
|
314 | 324 |
} |
315 | 325 |
if request.method == 'POST' and submit_name in request.POST: |
316 | 326 |
form = form_class(data=request.POST) |
... | ... | |
322 | 332 |
else: |
323 | 333 |
block['form'] = form_class() |
324 | 334 |
blocks.append(block) |
325 |
else: # New frontends API |
|
335 |
else: # New frontends API
|
|
326 | 336 |
parameters = {'request': request, |
327 | 337 |
'context': context} |
328 | 338 |
block = utils.get_authenticator_method(authenticator, 'login', parameters) |
... | ... | |
337 | 347 |
else: |
338 | 348 |
blocks[-1]['is_hidden'] = False |
339 | 349 | |
340 | ||
341 | 350 |
# Old frontends API |
342 | 351 |
for block in blocks: |
343 | 352 |
fid = block['id'] |
344 |
if not 'form' in block:
|
|
353 |
if 'form' not in block:
|
|
345 | 354 |
continue |
346 | 355 |
authenticator = block['authenticator'] |
347 | 356 |
context.update({ |
348 |
'submit_name': 'submit-%s' % fid,
|
|
349 |
redirect_field_name: redirect_to,
|
|
350 |
'form': block['form']
|
|
357 |
'submit_name': 'submit-%s' % fid, |
|
358 |
redirect_field_name: redirect_to, |
|
359 |
'form': block['form'] |
|
351 | 360 |
}) |
352 | 361 |
if hasattr(authenticator, 'get_context'): |
353 | 362 |
context.update(authenticator.get_context()) |
354 | 363 |
sub_template_name = authenticator.template() |
355 |
block['content'] = render_to_string( |
|
356 |
sub_template_name, context, |
|
357 |
request=request) |
|
364 |
block['content'] = render_to_string(sub_template_name, context, request=request) |
|
358 | 365 | |
359 | 366 |
request.session.set_test_cookie() |
360 | 367 | |
... | ... | |
423 | 430 |
for field_name in getattr(request.user, 'USER_PROFILE', []): |
424 | 431 |
if field_name not in field_names: |
425 | 432 |
field_names.append(field_name) |
426 |
qs = models.Attribute.objects.filter(Q(user_editable=True)|Q(user_visible=True))
|
|
433 |
qs = models.Attribute.objects.filter(Q(user_editable=True) | Q(user_visible=True))
|
|
427 | 434 |
qs = qs.values_list('name', flat=True) |
428 | 435 |
for field_name in qs: |
429 | 436 |
if field_name not in field_names: |
... | ... | |
479 | 486 |
# Credentials management |
480 | 487 |
parameters = {'request': request, |
481 | 488 |
'context': context} |
482 |
profiles = [utils.get_authenticator_method(frontend, 'profile', parameters) |
|
483 |
for frontend in frontends] |
|
489 |
profiles = [utils.get_authenticator_method(frontend, 'profile', parameters) for frontend in frontends] |
|
484 | 490 |
# Old frontends data structure for templates |
485 | 491 |
blocks = [block['content'] for block in profiles if block] |
486 | 492 |
# New frontends data structure for templates |
... | ... | |
510 | 516 | |
511 | 517 |
profile = login_required(ProfileView.as_view()) |
512 | 518 | |
519 | ||
513 | 520 |
def logout_list(request): |
514 | 521 |
'''Return logout links from idp backends''' |
515 | 522 |
return utils.accumulate_from_backends(request, 'logout_list') |
516 | 523 | |
524 | ||
517 | 525 |
def redirect_logout_list(request): |
518 | 526 |
'''Return redirect logout links from idp backends''' |
519 | 527 |
return utils.accumulate_from_backends(request, 'redirect_logout_list') |
520 | 528 | |
521 |
def logout(request, next_url=None, default_next_url='auth_homepage', |
|
522 |
redirect_field_name=REDIRECT_FIELD_NAME, |
|
523 |
template='authentic2/logout.html', do_local=True, check_referer=True): |
|
529 | ||
530 |
def logout(request, |
|
531 |
next_url=None, |
|
532 |
default_next_url='auth_homepage', |
|
533 |
redirect_field_name=REDIRECT_FIELD_NAME, |
|
534 |
template='authentic2/logout.html', |
|
535 |
do_local=True, |
|
536 |
check_referer=True): |
|
524 | 537 |
'''Logout first check if a logout request is authorized, i.e. |
525 | 538 |
that logout was done using a POST with CSRF token or with a GET |
526 | 539 |
from the same site. |
... | ... | |
528 | 541 |
Logout endpoints of IdP module must re-user the view by setting |
529 | 542 |
check_referer and do_local to False. |
530 | 543 |
''' |
531 |
logger = logging.getLogger(__name__) |
|
532 | 544 |
default_next_url = utils.make_url(default_next_url) |
533 |
next_url = next_url or request.GET.get(redirect_field_name, |
|
534 |
default_next_url) |
|
545 |
next_url = next_url or request.GET.get(redirect_field_name, default_next_url) |
|
535 | 546 |
ctx = {} |
536 | 547 |
ctx['next_url'] = next_url |
537 | 548 |
ctx['redir_timeout'] = 60 |
... | ... | |
541 | 552 |
return render(request, 'authentic2/logout_confirm.html', ctx) |
542 | 553 |
do_local = do_local and 'local' in request.GET |
543 | 554 |
if not do_local: |
544 |
l = logout_list(request)
|
|
545 |
if l:
|
|
555 |
fragments = logout_list(request)
|
|
556 |
if fragments:
|
|
546 | 557 |
# Full logout with iframes |
547 | 558 |
next_url = utils.make_url('auth_logout', params={ |
548 | 559 |
'local': 'ok', |
549 | 560 |
REDIRECT_FIELD_NAME: next_url}) |
550 | 561 |
ctx['next_url'] = next_url |
551 |
ctx['logout_list'] = l
|
|
562 |
ctx['logout_list'] = fragments
|
|
552 | 563 |
ctx['message'] = _('Logging out from all your services') |
553 | 564 |
return render(request, template, ctx) |
554 | 565 |
# Get redirection targets for full logout with redirections |
... | ... | |
613 | 624 | |
614 | 625 |
logged_in = never_cache(LoggedInView.as_view()) |
615 | 626 | |
627 | ||
616 | 628 |
def csrf_failure_view(request, reason=""): |
617 | 629 |
messages.warning(request, _('The page is out of date, it was reloaded for you')) |
618 | 630 |
return HttpResponseRedirect(request.get_full_path()) |
619 | 631 | |
620 |
def test_redirect(request): |
|
621 |
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL) |
|
622 |
messages.info(request, 'Une info') |
|
623 |
messages.warning(request, 'Un warning') |
|
624 |
messages.error(request, 'Une erreur') |
|
625 |
return HttpResponseRedirect(next_url) |
|
632 | ||
633 |
class PasswordResetView(cbv.NextURLViewMixin, FormView): |
|
634 |
'''Ask for an email and send a password reset link by mail''' |
|
635 |
form_class = passwords_forms.PasswordResetForm |
|
636 |
title = _('Password Reset') |
|
637 | ||
638 |
def get_template_names(self): |
|
639 |
return [ |
|
640 |
'authentic2/password_reset_form.html', |
|
641 |
'registration/password_reset_form.html', |
|
642 |
] |
|
643 | ||
644 |
def get_form_kwargs(self, **kwargs): |
|
645 |
kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs) |
|
646 |
initial = kwargs.setdefault('initial', {}) |
|
647 |
initial['next_url'] = self.request.GET.get(REDIRECT_FIELD_NAME, '') |
|
648 |
return kwargs |
|
649 | ||
650 |
def get_context_data(self, **kwargs): |
|
651 |
ctx = super(PasswordResetView, self).get_context_data(**kwargs) |
|
652 |
if app_settings.A2_USER_CAN_RESET_PASSWORD is False: |
|
653 |
raise Http404('Password reset is not allowed.') |
|
654 |
ctx['title'] = _('Password reset') |
|
655 |
return ctx |
|
656 | ||
657 |
def form_valid(self, form): |
|
658 |
form.save() |
|
659 |
# return to next URL |
|
660 |
messages.info(self.request, _('If your email address exists in our ' |
|
661 |
'database, you will receive an email ' |
|
662 |
'containing instructions to reset ' |
|
663 |
'your password')) |
|
664 |
return super(PasswordResetView, self).form_valid(form) |
|
665 | ||
666 |
password_reset = PasswordResetView.as_view() |
|
667 | ||
668 | ||
669 |
class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView): |
|
670 |
'''Validate password reset link, show a set password form and login |
|
671 |
the user. |
|
672 |
''' |
|
673 |
form_class = passwords_forms.SetPasswordForm |
|
674 |
title = _('Password Reset') |
|
675 | ||
676 |
def get_template_names(self): |
|
677 |
return [ |
|
678 |
'registration/password_reset_confirm.html', |
|
679 |
'authentic2/password_reset_confirm.html', |
|
680 |
] |
|
681 | ||
682 |
def dispatch(self, request, *args, **kwargs): |
|
683 |
validlink = True |
|
684 |
uidb64 = kwargs['uidb64'] |
|
685 |
self.token = token = kwargs['token'] |
|
686 | ||
687 |
UserModel = get_user_model() |
|
688 |
# checked by URLconf |
|
689 |
assert uidb64 is not None and token is not None |
|
690 |
try: |
|
691 |
uid = urlsafe_base64_decode(uidb64) |
|
692 |
# use authenticate to eventually get an LDAPUser |
|
693 |
self.user = authenticate(user=UserModel._default_manager.get(pk=uid)) |
|
694 |
except (TypeError, ValueError, OverflowError, |
|
695 |
UserModel.DoesNotExist): |
|
696 |
validlink = False |
|
697 |
messages.warning(request, _('User not found')) |
|
698 | ||
699 |
if validlink and not compat.default_token_generator.check_token(self.user, token): |
|
700 |
validlink = False |
|
701 |
messages.warning(request, _('You reset password link is invalid or has expired')) |
|
702 |
if not validlink: |
|
703 |
return utils.redirect(request, self.get_success_url()) |
|
704 |
can_reset_password = utils.get_user_flag(user=self.user, |
|
705 |
name='can_reset_password', |
|
706 |
default=self.user.has_usable_password()) |
|
707 |
if not can_reset_password: |
|
708 |
messages.warning( |
|
709 |
request, |
|
710 |
_('It\'s not possible to reset your password. Please contact an administrator.')) |
|
711 |
return utils.redirect(request, self.get_success_url()) |
|
712 |
return super(PasswordResetConfirmView, self).dispatch(request, *args, |
|
713 |
**kwargs) |
|
714 | ||
715 |
def get_context_data(self, **kwargs): |
|
716 |
ctx = super(PasswordResetConfirmView, self).get_context_data(**kwargs) |
|
717 |
# compatibility with existing templates ! |
|
718 |
ctx['title'] = _('Enter new password') |
|
719 |
ctx['validlink'] = True |
|
720 |
return ctx |
|
721 | ||
722 |
def get_form_kwargs(self): |
|
723 |
kwargs = super(PasswordResetConfirmView, self).get_form_kwargs() |
|
724 |
kwargs['user'] = self.user |
|
725 |
return kwargs |
|
726 | ||
727 |
def form_valid(self, form): |
|
728 |
# Changing password by mail validate the email |
|
729 |
form.user.email_verified = True |
|
730 |
form.save() |
|
731 |
hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token, |
|
732 |
form=form) |
|
733 |
logger.info(u'user %s resetted its password with token %r...', |
|
734 |
self.user, self.token[:9]) |
|
735 |
return self.finish() |
|
736 | ||
737 |
def finish(self): |
|
738 |
return utils.simulate_authentication(self.request, self.user, 'email') |
|
739 | ||
740 |
password_reset_confirm = PasswordResetConfirmView.as_view() |
|
741 | ||
742 | ||
743 |
def switch_back(request): |
|
744 |
return utils.switch_back(request) |
|
745 | ||
746 | ||
747 |
def valid_token(method): |
|
748 |
def f(request, *args, **kwargs): |
|
749 |
try: |
|
750 |
request.token = signing.loads(kwargs['registration_token'].replace(' ', ''), |
|
751 |
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
752 |
except signing.SignatureExpired: |
|
753 |
messages.warning(request, _('Your activation key is expired')) |
|
754 |
return utils.redirect(request, 'registration_register') |
|
755 |
except signing.BadSignature: |
|
756 |
messages.warning(request, _('Activation failed')) |
|
757 |
return utils.redirect(request, 'registration_register') |
|
758 |
return method(request, *args, **kwargs) |
|
759 |
return f |
|
760 | ||
761 | ||
762 |
class BaseRegistrationView(FormView): |
|
763 |
form_class = registration_forms.RegistrationForm |
|
764 |
template_name = 'registration/registration_form.html' |
|
765 |
title = _('Registration') |
|
766 | ||
767 |
def dispatch(self, request, *args, **kwargs): |
|
768 |
if not getattr(settings, 'REGISTRATION_OPEN', True): |
|
769 |
raise Http404('Registration is not open.') |
|
770 |
self.token = {} |
|
771 |
self.ou = get_default_ou() |
|
772 |
# load pre-filled values |
|
773 |
if request.GET.get('token'): |
|
774 |
try: |
|
775 |
self.token = signing.loads( |
|
776 |
request.GET.get('token'), |
|
777 |
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
778 |
except (TypeError, ValueError, signing.BadSignature) as e: |
|
779 |
logger.warning(u'registration_view: invalid token: %s', e) |
|
780 |
return HttpResponseBadRequest('invalid token', content_type='text/plain') |
|
781 |
if 'ou' in self.token: |
|
782 |
self.ou = OU.objects.get(pk=self.token['ou']) |
|
783 |
self.next_url = self.token.pop(REDIRECT_FIELD_NAME, utils.select_next_url(request, None)) |
|
784 |
return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs) |
|
785 | ||
786 |
def form_valid(self, form): |
|
787 |
email = form.cleaned_data.pop('email') |
|
788 |
for field in form.cleaned_data: |
|
789 |
self.token[field] = form.cleaned_data[field] |
|
790 | ||
791 |
# propagate service to the registration completion view |
|
792 |
if constants.SERVICE_FIELD_NAME in self.request.GET: |
|
793 |
self.token[constants.SERVICE_FIELD_NAME] = \ |
|
794 |
self.request.GET[constants.SERVICE_FIELD_NAME] |
|
795 | ||
796 |
self.token.pop(REDIRECT_FIELD_NAME, None) |
|
797 |
self.token.pop('email', None) |
|
798 | ||
799 |
utils.send_registration_mail(self.request, email, next_url=self.next_url, |
|
800 |
ou=self.ou, **self.token) |
|
801 |
self.request.session['registered_email'] = email |
|
802 |
return utils.redirect(self.request, 'registration_complete', params={REDIRECT_FIELD_NAME: self.next_url}) |
|
803 | ||
804 |
def get_context_data(self, **kwargs): |
|
805 |
context = super(BaseRegistrationView, self).get_context_data(**kwargs) |
|
806 |
parameters = {'request': self.request, |
|
807 |
'context': context} |
|
808 |
blocks = [utils.get_authenticator_method(authenticator, 'registration', parameters) |
|
809 |
for authenticator in utils.get_backends('AUTH_FRONTENDS')] |
|
810 |
context['frontends'] = collections.OrderedDict((block['id'], block) |
|
811 |
for block in blocks if block) |
|
812 |
return context |
|
813 | ||
814 | ||
815 |
class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView): |
|
816 |
pass |
|
817 | ||
818 | ||
819 |
class RegistrationCompletionView(CreateView): |
|
820 |
model = get_user_model() |
|
821 |
success_url = 'auth_homepage' |
|
822 | ||
823 |
def get_template_names(self): |
|
824 |
if self.users and 'create' not in self.request.GET: |
|
825 |
return ['registration/registration_completion_choose.html'] |
|
826 |
else: |
|
827 |
return ['registration/registration_completion_form.html'] |
|
828 | ||
829 |
def get_success_url(self): |
|
830 |
try: |
|
831 |
redirect_url, next_field = app_settings.A2_REGISTRATION_REDIRECT |
|
832 |
except Exception: |
|
833 |
redirect_url = app_settings.A2_REGISTRATION_REDIRECT |
|
834 |
next_field = REDIRECT_FIELD_NAME |
|
835 | ||
836 |
if self.token and self.token.get(REDIRECT_FIELD_NAME): |
|
837 |
url = self.token[REDIRECT_FIELD_NAME] |
|
838 |
if redirect_url: |
|
839 |
url = utils.make_url(redirect_url, params={next_field: url}) |
|
840 |
else: |
|
841 |
if redirect_url: |
|
842 |
url = redirect_url |
|
843 |
else: |
|
844 |
url = utils.make_url(self.success_url) |
|
845 |
return url |
|
846 | ||
847 |
def dispatch(self, request, *args, **kwargs): |
|
848 |
self.token = request.token |
|
849 |
self.authentication_method = self.token.get('authentication_method', 'email') |
|
850 |
self.email = request.token['email'] |
|
851 |
if 'ou' in self.token: |
|
852 |
self.ou = OU.objects.get(pk=self.token['ou']) |
|
853 |
else: |
|
854 |
self.ou = get_default_ou() |
|
855 |
self.users = User.objects.filter(email__iexact=self.email) \ |
|
856 |
.order_by('date_joined') |
|
857 |
if self.ou: |
|
858 |
self.users = self.users.filter(ou=self.ou) |
|
859 |
self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE \ |
|
860 |
or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE |
|
861 |
if self.ou: |
|
862 |
self.email_is_unique |= self.ou.email_is_unique |
|
863 |
self.init_fields_labels_and_help_texts() |
|
864 |
# if registration is done during an SSO add the service to the registration event |
|
865 |
self.service = self.token.get(constants.SERVICE_FIELD_NAME) |
|
866 |
return super(RegistrationCompletionView, self) \ |
|
867 |
.dispatch(request, *args, **kwargs) |
|
868 | ||
869 |
def init_fields_labels_and_help_texts(self): |
|
870 |
attributes = models.Attribute.objects.filter( |
|
871 |
asked_on_registration=True) |
|
872 |
default_fields = attributes.values_list('name', flat=True) |
|
873 |
required_fields = models.Attribute.objects.filter(required=True) \ |
|
874 |
.values_list('name', flat=True) |
|
875 |
fields, labels = utils.get_fields_and_labels( |
|
876 |
app_settings.A2_REGISTRATION_FIELDS, |
|
877 |
default_fields, |
|
878 |
app_settings.A2_REGISTRATION_REQUIRED_FIELDS, |
|
879 |
app_settings.A2_REQUIRED_FIELDS, |
|
880 |
models.Attribute.objects.filter(required=True).values_list('name', flat=True)) |
|
881 |
help_texts = {} |
|
882 |
if app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL: |
|
883 |
labels['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL |
|
884 |
if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT: |
|
885 |
help_texts['username'] = \ |
|
886 |
app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT |
|
887 |
required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + \ |
|
888 |
list(required_fields) |
|
889 |
if 'email' in fields: |
|
890 |
fields.remove('email') |
|
891 |
for field in self.token.get('skip_fields') or []: |
|
892 |
if field in fields: |
|
893 |
fields.remove(field) |
|
894 |
self.fields = fields |
|
895 |
self.labels = labels |
|
896 |
self.required = required |
|
897 |
self.help_texts = help_texts |
|
898 | ||
899 |
def get_form_class(self): |
|
900 |
if not self.token.get('valid_email', True): |
|
901 |
self.fields.append('email') |
|
902 |
self.required.append('email') |
|
903 |
form_class = registration_forms.RegistrationCompletionForm |
|
904 |
if self.token.get('no_password', False): |
|
905 |
form_class = registration_forms.RegistrationCompletionFormNoPassword |
|
906 |
form_class = profile_forms.modelform_factory( |
|
907 |
self.model, |
|
908 |
form=form_class, |
|
909 |
fields=self.fields, |
|
910 |
labels=self.labels, |
|
911 |
required=self.required, |
|
912 |
help_texts=self.help_texts) |
|
913 |
if 'username' in self.fields and app_settings.A2_REGISTRATION_FORM_USERNAME_REGEX: |
|
914 |
# Keep existing field label and help_text |
|
915 |
old_field = form_class.base_fields['username'] |
|
916 |
field = CharField( |
|
917 |
max_length=256, |
|
918 |
label=old_field.label, |
|
919 |
help_text=old_field.help_text, |
|
920 |
validators=[validators.UsernameValidator()]) |
|
921 |
form_class = type('RegistrationForm', (form_class,), {'username': field}) |
|
922 |
return form_class |
|
923 | ||
924 |
def get_form_kwargs(self, **kwargs): |
|
925 |
'''Initialize mail from token''' |
|
926 |
kwargs = super(RegistrationCompletionView, self).get_form_kwargs(**kwargs) |
|
927 |
if 'ou' in self.token: |
|
928 |
ou = get_object_or_404(OU, id=self.token['ou']) |
|
929 |
else: |
|
930 |
ou = get_default_ou() |
|
931 | ||
932 |
attributes = {'email': self.email, 'ou': ou} |
|
933 |
for key in self.token: |
|
934 |
if key in app_settings.A2_PRE_REGISTRATION_FIELDS: |
|
935 |
attributes[key] = self.token[key] |
|
936 |
logger.debug(u'attributes %s', attributes) |
|
937 | ||
938 |
prefilling_list = utils.accumulate_from_backends(self.request, 'registration_form_prefill') |
|
939 |
logger.debug(u'prefilling_list %s', prefilling_list) |
|
940 |
# Build a single meaningful prefilling with sets of values |
|
941 |
prefilling = {} |
|
942 |
for p in prefilling_list: |
|
943 |
for name, values in p.items(): |
|
944 |
if name in self.fields: |
|
945 |
prefilling.setdefault(name, set()).update(values) |
|
946 |
logger.debug(u'prefilling %s', prefilling) |
|
947 | ||
948 |
for name, values in prefilling.items(): |
|
949 |
attributes[name] = ' '.join(values) |
|
950 |
logger.debug(u'attributes with prefilling %s', attributes) |
|
951 | ||
952 |
if self.token.get('user_id'): |
|
953 |
kwargs['instance'] = User.objects.get(id=self.token.get('user_id')) |
|
954 |
else: |
|
955 |
init_kwargs = {} |
|
956 |
for key in ('email', 'first_name', 'last_name', 'ou'): |
|
957 |
if key in attributes: |
|
958 |
init_kwargs[key] = attributes[key] |
|
959 |
kwargs['instance'] = get_user_model()(**init_kwargs) |
|
960 | ||
961 |
return kwargs |
|
962 | ||
963 |
def get_form(self, form_class=None): |
|
964 |
form = super(RegistrationCompletionView, self).get_form(form_class=form_class) |
|
965 |
hooks.call_hooks('front_modify_form', self, form) |
|
966 |
return form |
|
967 | ||
968 |
def get_context_data(self, **kwargs): |
|
969 |
ctx = super(RegistrationCompletionView, self).get_context_data(**kwargs) |
|
970 |
ctx['token'] = self.token |
|
971 |
ctx['users'] = self.users |
|
972 |
ctx['email'] = self.email |
|
973 |
ctx['email_is_unique'] = self.email_is_unique |
|
974 |
ctx['create'] = 'create' in self.request.GET |
|
975 |
return ctx |
|
976 | ||
977 |
def get(self, request, *args, **kwargs): |
|
978 |
if len(self.users) == 1 and self.email_is_unique: |
|
979 |
# Found one user, EMAIL is unique, log her in |
|
980 |
utils.simulate_authentication( |
|
981 |
request, self.users[0], |
|
982 |
method=self.authentication_method, |
|
983 |
service_slug=self.service) |
|
984 |
return utils.redirect(request, self.get_success_url()) |
|
985 |
confirm_data = self.token.get('confirm_data', False) |
|
986 | ||
987 |
if confirm_data == 'required': |
|
988 |
fields_to_confirm = self.required |
|
989 |
else: |
|
990 |
fields_to_confirm = self.fields |
|
991 |
if (all(field in self.token for field in fields_to_confirm) |
|
992 |
and (not confirm_data or confirm_data == 'required')): |
|
993 |
# We already have every fields |
|
994 |
form_kwargs = self.get_form_kwargs() |
|
995 |
form_class = self.get_form_class() |
|
996 |
data = self.token |
|
997 |
if 'password' in data: |
|
998 |
data['password1'] = data['password'] |
|
999 |
data['password2'] = data['password'] |
|
1000 |
del data['password'] |
|
1001 |
form_kwargs['data'] = data |
|
1002 |
form = form_class(**form_kwargs) |
|
1003 |
if form.is_valid(): |
|
1004 |
user = form.save() |
|
1005 |
return self.registration_success(request, user, form) |
|
1006 |
self.get_form = lambda *args, **kwargs: form |
|
1007 |
return super(RegistrationCompletionView, self).get(request, *args, **kwargs) |
|
1008 | ||
1009 |
def post(self, request, *args, **kwargs): |
|
1010 |
if self.users and self.email_is_unique: |
|
1011 |
# email is unique, users already exist, creating a new one is forbidden ! |
|
1012 |
return utils.redirect( |
|
1013 |
request, request.resolver_match.view_name, args=self.args, |
|
1014 |
kwargs=self.kwargs) |
|
1015 |
if 'uid' in request.POST: |
|
1016 |
uid = request.POST['uid'] |
|
1017 |
for user in self.users: |
|
1018 |
if str(user.id) == uid: |
|
1019 |
utils.simulate_authentication( |
|
1020 |
request, user, |
|
1021 |
method=self.authentication_method, |
|
1022 |
service_slug=self.service) |
|
1023 |
return utils.redirect(request, self.get_success_url()) |
|
1024 |
return super(RegistrationCompletionView, self).post(request, *args, **kwargs) |
|
1025 | ||
1026 |
def form_valid(self, form): |
|
1027 | ||
1028 |
# remove verified fields from form, this allows an authentication |
|
1029 |
# method to provide verified data fields and to present it to the user, |
|
1030 |
# while preventing the user to modify them. |
|
1031 |
for av in models.AttributeValue.objects.with_owner(form.instance): |
|
1032 |
if av.verified and av.attribute.name in form.fields: |
|
1033 |
del form.fields[av.attribute.name] |
|
1034 | ||
1035 |
if ('email' in self.request.POST |
|
1036 |
and ('email' not in self.token or self.request.POST['email'] != self.token['email']) |
|
1037 |
and not self.token.get('skip_email_check')): |
|
1038 |
# If an email is submitted it must be validated or be the same as in the token |
|
1039 |
data = form.cleaned_data |
|
1040 |
data['no_password'] = self.token.get('no_password', False) |
|
1041 |
utils.send_registration_mail( |
|
1042 |
self.request, |
|
1043 |
ou=self.ou, |
|
1044 |
next_url=self.get_success_url(), |
|
1045 |
**data) |
|
1046 |
self.request.session['registered_email'] = form.cleaned_data['email'] |
|
1047 |
return utils.redirect(self.request, 'registration_complete') |
|
1048 |
super(RegistrationCompletionView, self).form_valid(form) |
|
1049 |
return self.registration_success(self.request, form.instance, form) |
|
1050 | ||
1051 |
def registration_success(self, request, user, form): |
|
1052 |
hooks.call_hooks('event', name='registration', user=user, form=form, view=self, |
|
1053 |
authentication_method=self.authentication_method, |
|
1054 |
token=request.token, service=self.service) |
|
1055 |
utils.simulate_authentication( |
|
1056 |
request, user, |
|
1057 |
method=self.authentication_method, |
|
1058 |
service_slug=self.service) |
|
1059 |
messages.info(self.request, _('You have just created an account.')) |
|
1060 |
self.send_registration_success_email(user) |
|
1061 |
return utils.redirect(request, self.get_success_url()) |
|
1062 | ||
1063 |
def send_registration_success_email(self, user): |
|
1064 |
if not user.email: |
|
1065 |
return |
|
1066 | ||
1067 |
template_names = [ |
|
1068 |
'authentic2/registration_success' |
|
1069 |
] |
|
1070 |
login_url = self.request.build_absolute_uri(settings.LOGIN_URL) |
|
1071 |
utils.send_templated_mail(user, template_names=template_names, |
|
1072 |
context={ |
|
1073 |
'user': user, |
|
1074 |
'email': user.email, |
|
1075 |
'site': self.request.get_host(), |
|
1076 |
'login_url': login_url, |
|
1077 |
}, |
|
1078 |
request=self.request) |
|
1079 | ||
1080 | ||
1081 |
class DeleteView(FormView): |
|
1082 |
template_name = 'authentic2/accounts_delete.html' |
|
1083 |
success_url = reverse_lazy('auth_logout') |
|
1084 |
title = _('Delete account') |
|
1085 | ||
1086 |
def dispatch(self, request, *args, **kwargs): |
|
1087 |
if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT: |
|
1088 |
return utils.redirect(request, '..') |
|
1089 |
return super(DeleteView, self).dispatch(request, *args, **kwargs) |
|
1090 | ||
1091 |
def post(self, request, *args, **kwargs): |
|
1092 |
if 'cancel' in request.POST: |
|
1093 |
return utils.redirect(request, 'account_management') |
|
1094 |
return super(DeleteView, self).post(request, *args, **kwargs) |
|
1095 | ||
1096 |
def get_form_class(self): |
|
1097 |
if self.request.user.has_usable_password(): |
|
1098 |
return profile_forms.DeleteAccountForm |
|
1099 |
return Form |
|
1100 | ||
1101 |
def get_form_kwargs(self, **kwargs): |
|
1102 |
kwargs = super(DeleteView, self).get_form_kwargs(**kwargs) |
|
1103 |
if self.request.user.has_usable_password(): |
|
1104 |
kwargs['user'] = self.request.user |
|
1105 |
return kwargs |
|
1106 | ||
1107 |
def form_valid(self, form): |
|
1108 |
utils.send_account_deletion_mail(self.request, self.request.user) |
|
1109 |
models.DeletedUser.objects.delete_user(self.request.user) |
|
1110 |
self.request.user.email += '#%d' % random.randint(1, 10000000) |
|
1111 |
self.request.user.email_verified = False |
|
1112 |
self.request.user.save(update_fields=['email', 'email_verified']) |
|
1113 |
logger.info(u'deletion of account %s requested', self.request.user) |
|
1114 |
hooks.call_hooks('event', name='delete-account', user=self.request.user) |
|
1115 |
messages.info(self.request, |
|
1116 |
_('Your account has been scheduled for deletion. You cannot use it anymore.')) |
|
1117 |
return super(DeleteView, self).form_valid(form) |
|
1118 | ||
1119 |
registration_completion = valid_token(RegistrationCompletionView.as_view()) |
|
1120 | ||
1121 | ||
1122 |
class RegistrationCompleteView(TemplateView): |
|
1123 |
template_name = 'registration/registration_complete.html' |
|
1124 | ||
1125 |
def get_context_data(self, **kwargs): |
|
1126 |
kwargs['next_url'] = utils.select_next_url(self.request, settings.LOGIN_REDIRECT_URL) |
|
1127 |
return super(RegistrationCompleteView, self).get_context_data( |
|
1128 |
account_activation_days=settings.ACCOUNT_ACTIVATION_DAYS, |
|
1129 |
**kwargs) |
|
1130 | ||
1131 | ||
1132 |
registration_complete = RegistrationCompleteView.as_view() |
|
1133 | ||
1134 | ||
1135 |
@sensitive_post_parameters() |
|
1136 |
@login_required |
|
1137 |
@decorators.setting_enabled('A2_REGISTRATION_CAN_CHANGE_PASSWORD') |
|
1138 |
def password_change(request, *args, **kwargs): |
|
1139 |
kwargs['password_change_form'] = passwords_forms.PasswordChangeForm |
|
1140 |
post_change_redirect = kwargs.pop('post_change_redirect', None) |
|
1141 |
if 'next_url' in request.POST and request.POST['next_url']: |
|
1142 |
post_change_redirect = request.POST['next_url'] |
|
1143 |
elif REDIRECT_FIELD_NAME in request.GET: |
|
1144 |
post_change_redirect = request.GET[REDIRECT_FIELD_NAME] |
|
1145 |
elif post_change_redirect is None: |
|
1146 |
post_change_redirect = reverse('account_management') |
|
1147 |
if not utils.user_can_change_password(request=request): |
|
1148 |
messages.warning(request, _('Password change is forbidden')) |
|
1149 |
return utils.redirect(request, post_change_redirect) |
|
1150 |
if 'cancel' in request.POST: |
|
1151 |
return utils.redirect(request, post_change_redirect) |
|
1152 |
kwargs['post_change_redirect'] = post_change_redirect |
|
1153 |
extra_context = kwargs.setdefault('extra_context', {}) |
|
1154 |
extra_context['view'] = password_change |
|
1155 |
extra_context[REDIRECT_FIELD_NAME] = post_change_redirect |
|
1156 |
if not request.user.has_usable_password(): |
|
1157 |
kwargs['password_change_form'] = passwords_forms.SetPasswordForm |
|
1158 |
response = dj_password_change(request, *args, **kwargs) |
|
1159 |
if isinstance(response, HttpResponseRedirect): |
|
1160 |
hooks.call_hooks('event', name='change-password', user=request.user, request=request) |
|
1161 |
messages.info(request, _('Password changed')) |
|
1162 |
return response |
|
1163 |
password_change.title = _('Password Change') |
|
1164 |
password_change.do_not_call_in_templates = True |
|
1165 | ||
1166 | ||
1167 |
def notimplemented_view(request): |
|
1168 |
raise NotImplementedError |
src/authentic2/widgets.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
# legacy module, please use authentic2.forms.widgets now. |
2 |
from .forms.widgets import * |
|
18 |
from .forms.widgets import * # noqa: F403,F401 |
src/authentic2/wsgi.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
""" |
2 | 18 |
WSGI config for a authentic2 project. |
3 | 19 | |
... | ... | |
15 | 31 |
""" |
16 | 32 |
import os |
17 | 33 | |
18 |
from . import logger |
|
34 |
# XXX: monkeypatch logging |
|
35 |
from . import logger # noqa: F401 |
|
36 |
from django.core.wsgi import get_wsgi_application |
|
19 | 37 | |
20 | 38 |
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentic2.settings") |
21 | 39 | |
22 | 40 |
# This application object is used by any WSGI server configured to use this |
23 | 41 |
# file. This includes Django's development server, if the WSGI_APPLICATION |
24 | 42 |
# setting points here. |
25 |
from django.core.wsgi import get_wsgi_application |
|
26 | 43 |
application = get_wsgi_application() |
src/authentic2_auth_oidc/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 |
from django.utils.translation import ugettext_lazy as _ |
|
4 | 19 |
from django.core.urlresolvers import reverse |
5 | 20 | |
6 | 21 |
from authentic2.utils import make_url |
src/authentic2_auth_oidc/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.contrib import admin |
2 | 18 | |
3 | 19 |
from . import models |
src/authentic2_auth_oidc/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 | ||
19 | ||
1 | 20 |
class AppSettings(object): |
2 | 21 |
'''Thanks django-allauth''' |
3 | 22 |
__SENTINEL = object() |
... | ... | |
18 | 37 |
def ENABLE(self): |
19 | 38 |
return self._setting('ENABLE', True) |
20 | 39 | |
21 | ||
22 |
import sys |
|
23 | ||
24 | 40 |
app_settings = AppSettings('A2_AUTH_OIDC_') |
25 | 41 |
app_settings.__name__ = __name__ |
26 | 42 |
sys.modules[__name__] = app_settings |
src/authentic2_auth_oidc/authenticators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import gettext_noop |
2 | 18 |
from django.shortcuts import render |
3 | 19 |
src/authentic2_auth_oidc/backends.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 |
import datetime |
3 | 19 |
src/authentic2_auth_oidc/management/commands/oidc-register-issuer.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from __future__ import print_function |
2 | 18 | |
3 | 19 |
import json |
... | ... | |
6 | 22 | |
7 | 23 |
from django.core.management.base import BaseCommand, CommandError |
8 | 24 |
from django.core.exceptions import ValidationError |
9 |
from django.utils.six import text_type
|
|
25 |
from django.db.transaction import atomic
|
|
10 | 26 | |
11 |
from authentic2.compat import atomic |
|
12 | 27 | |
13 | 28 |
from authentic2_auth_oidc.utils import register_issuer |
14 | 29 |
from authentic2_auth_oidc.models import OIDCClaimMapping, OIDCProvider |
src/authentic2_auth_oidc/managers.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.db.models.query import QuerySet |
2 | 18 | |
3 | 19 |
src/authentic2_auth_oidc/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import uuid |
2 | 18 |
import json |
3 | 19 |
src/authentic2_auth_oidc/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url |
2 | 18 | |
3 | 19 |
from . import views |
src/authentic2_auth_oidc/utils.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import datetime |
2 | 18 |
import base64 |
3 | 19 |
import json |
... | ... | |
60 | 76 |
def parse_id_token(id_token): |
61 | 77 |
try: |
62 | 78 |
id_token = str(id_token) |
63 |
except UnicodeDecodeError as e:
|
|
79 |
except UnicodeDecodeError: |
|
64 | 80 |
raise ValueError('invalid characters in id_token') |
65 | 81 |
payload = id_token.split('.') |
66 | 82 |
if len(payload) == 5: |
... | ... | |
73 | 89 |
raise ValueError('header is not base64 decodable: %s' % e) |
74 | 90 |
try: |
75 | 91 |
headers = json.loads(headers) |
76 |
except ValueError as e:
|
|
92 |
except ValueError: |
|
77 | 93 |
raise ValueError('cannot JSON decode headers') |
78 | 94 |
if not isinstance(headers, dict): |
79 | 95 |
raise ValueError('JOSE header is not a dict %r' % headers) |
... | ... | |
250 | 266 |
old_pk = models.OIDCProvider.objects.get(issuer=openid_configuration['issuer']).pk |
251 | 267 |
except models.OIDCProvider.DoesNotExist: |
252 | 268 |
old_pk = None |
253 |
if (set(['RS256', 'RS384', 'RS512']) &
|
|
254 |
set(openid_configuration['id_token_signing_alg_values_supported'])): |
|
269 |
if (set(['RS256', 'RS384', 'RS512']) |
|
270 |
& set(openid_configuration['id_token_signing_alg_values_supported'])):
|
|
255 | 271 |
idtoken_algo = models.OIDCProvider.ALGO_RSA |
256 |
elif (set(['HS256', 'HS384', 'HS512']) &
|
|
257 |
set(openid_configuration['id_token_signing_alg_values_supported'])): |
|
272 |
elif (set(['HS256', 'HS384', 'HS512']) |
|
273 |
& set(openid_configuration['id_token_signing_alg_values_supported'])):
|
|
258 | 274 |
idtoken_algo = models.OIDCProvider.ALGO_HMAC |
259 | 275 |
else: |
260 | 276 |
raise ValueError(_('no common algorithm found for signing idtokens: %s') % |
src/authentic2_auth_oidc/views.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import uuid |
2 | 18 |
import logging |
3 | 19 |
import json |
src/authentic2_auth_saml/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 | ||
1 | 18 |
class Plugin(object): |
2 | 19 |
def get_before_urls(self): |
3 | 20 |
from . import urls |
src/authentic2_auth_saml/adapters.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
import logging |
2 | 18 | |
3 | 19 |
from mellon.adapters import DefaultAdapter |
src/authentic2_auth_saml/app_settings.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 | ||
1 | 19 | |
2 | 20 |
class AppSettings(object): |
3 | 21 |
'''Thanks django-allauth''' |
... | ... | |
19 | 37 |
def enable(self): |
20 | 38 |
return self._setting('ENABLE', False) |
21 | 39 | |
22 | ||
23 |
import sys |
|
24 | ||
25 | 40 |
app_settings = AppSettings('A2_AUTH_SAML_') |
26 | 41 |
app_settings.__name__ = __name__ |
27 | 42 |
sys.modules[__name__] = app_settings |
src/authentic2_auth_saml/authenticators.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.utils.translation import gettext_noop |
2 | 18 |
from django.template.loader import render_to_string |
3 | 19 |
from django.shortcuts import render |
src/authentic2_auth_saml/backends.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from mellon.backends import SAMLBackend |
2 | 18 | |
3 | 19 |
from authentic2.middleware import StoreRequestMiddleware |
src/authentic2_auth_saml/urls.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.conf.urls import url, include |
2 | 18 | |
3 | 19 |
urlpatterns = [url(r'^accounts/saml/', include('mellon.urls'))] |
src/authentic2_idp_cas/__init__.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django.template.loader import render_to_string |
2 |
from django.utils.translation import ugettext_lazy as _ |
|
3 | 18 | |
4 | 19 |
from .constants import SESSION_CAS_LOGOUTS |
5 | 20 | |
21 | ||
6 | 22 |
class Plugin(object): |
7 | 23 |
def get_before_urls(self): |
8 | 24 |
from . import app_settings |
... | ... | |
10 | 26 |
from authentic2.decorators import setting_enabled, required |
11 | 27 | |
12 | 28 |
return required( |
13 |
(
|
|
14 |
setting_enabled('ENABLE', settings=app_settings),
|
|
15 |
),
|
|
16 |
[url(r'^idp/cas/', include(__name__ + '.urls'))])
|
|
29 |
( |
|
30 |
setting_enabled('ENABLE', settings=app_settings), |
|
31 |
), |
|
32 |
[url(r'^idp/cas/', include(__name__ + '.urls'))]) |
|
17 | 33 | |
18 | 34 |
def get_apps(self): |
19 | 35 |
return [__name__] |
... | ... | |
30 | 46 |
} |
31 | 47 |
content = render_to_string('authentic2_idp_cas/logout_fragment.html', ctx) |
32 | 48 |
fragments.append(content) |
33 |
return fragments |
|
49 |
return fragments |
src/authentic2_idp_cas/admin.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
1 | 17 |
from django import forms |
2 | 18 |
from django.contrib import admin |
3 | 19 |
from django.utils.translation import ugettext as _ |
... | ... | |
8 | 24 | |
9 | 25 |
from . import models |
10 | 26 | |
27 | ||
11 | 28 |
class ServiceForm(forms.ModelForm): |
12 | 29 |
def __init__(self, *args, **kwargs): |
13 | 30 |
super(ServiceForm, self).__init__(*args, **kwargs) |
... | ... | |
23 | 40 |
model = models.Service |
24 | 41 |
fields = '__all__' |
25 | 42 | |
43 | ||
26 | 44 |
class AttributeInlineForm(forms.ModelForm): |
27 | 45 |
def __init__(self, *args, **kwargs): |
28 | 46 |
service = kwargs.pop('service', None) |
29 | 47 |
super(AttributeInlineForm, self).__init__(*args, **kwargs) |
30 | 48 |
choices = self.choices({ |
31 |
'user': None,
|
|
32 |
'request': None,
|
|
33 |
'service': service
|
|
49 |
'user': None, |
|
50 |
'request': None, |
|
51 |
'service': service |
|
34 | 52 |
}) |
35 | 53 |
self.fields['attribute_name'].choices = choices |
36 | 54 |
self.fields['attribute_name'].widget = forms.Select(choices=choices) |
... | ... | |
42 | 60 |
class Meta: |
43 | 61 |
model = models.Attribute |
44 | 62 |
fields = [ |
45 |
'slug',
|
|
46 |
'attribute_name',
|
|
47 |
'enabled',
|
|
63 |
'slug', |
|
64 |
'attribute_name', |
|
65 |
'enabled', |
|
48 | 66 |
] |
49 | 67 | |
68 | ||
50 | 69 |
class AttributeInlineAdmin(admin.TabularInline): |
51 | 70 |
model = models.Attribute |
52 | 71 |
form = AttributeInlineForm |
... | ... | |
60 | 79 |
kwargs['form'] = NewForm |
61 | 80 |
return super(AttributeInlineAdmin, self).get_formset(request, obj=obj, **kwargs) |
62 | 81 | |
82 | ||
63 | 83 |
class ServiceAdmin(admin.ModelAdmin): |
64 | 84 |
form = ServiceForm |
65 | 85 |
list_display = ('name', 'ou', 'slug', 'urls', 'identifier_attribute') |
66 | 86 |
prepopulated_fields = {"slug": ("name",)} |
67 | 87 |
fieldsets = ( |
68 |
(None, { |
|
69 |
'fields': [ |
|
70 |
'name', |
|
71 |
'slug', |
|
72 |
'ou', |
|
73 |
'urls', |
|
74 |
'identifier_attribute', |