deprecate plugin system based on pkg_resources (fixes #22865)
gitea/authentic/pipeline/head Build started...
Details
gitea/authentic/pipeline/head Build started...
Details
This commit is contained in:
parent
ed30de5b18
commit
be7aafaad0
|
@ -1,2 +0,0 @@
|
|||
authentic2-plugin-template is entirely under the copyright of Entr'ouvert and
|
||||
distributed under the license AGPLv3 or later.
|
|
@ -1,3 +0,0 @@
|
|||
include COPYING
|
||||
recursive-include src/authentic2_plugin_template/templates *.html
|
||||
recursive-include src/authentic2_plugin_template/static *.js *.css *.png
|
|
@ -1,20 +0,0 @@
|
|||
** THIS IS A TEMPLATE PROJECT **
|
||||
|
||||
To rename it to your taste:
|
||||
|
||||
$ ./adapt.sh
|
||||
|
||||
** THIS IS A TEMPLATE PROJECT **
|
||||
Authentic2 Plugin Template
|
||||
==========================
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
You just have to install the package in your virtualenv and relaunch, it will
|
||||
be automatically loaded by authentic2.
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
** DESCRIBE CUSTOM SETTINGS HERE **
|
|
@ -1,38 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -x
|
||||
|
||||
echo "Give project name (it must match regexp ^[a-z][a-z0-9-]+$ )"
|
||||
read PROJECT_NAME
|
||||
|
||||
if ! echo $PROJECT_NAME | grep -q '^[a-z][a-z0-9-]\+$'; then
|
||||
echo "Invalid project name:" $PROJECT_NAME
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UPPER_UNDERSCORED=`echo $PROJECT_NAME | tr a-z A-Z | sed 's/-/_/g'`
|
||||
LOWER_UNDERSCORED=`echo $PROJECT_NAME | sed 's/-/_/g'`
|
||||
TITLECASE=`echo $PROJECT_NAME | sed 's/-/ /g;s/.*/\L&/; s/[a-z]*/\u&/g'`
|
||||
|
||||
echo Project name: $PROJECT_NAME
|
||||
echo Uppercase underscored: $UPPER_UNDERSCORED
|
||||
echo Lowercase underscored: $LOWER_UNDERSCORED
|
||||
echo Titlecase: $TITLECASE
|
||||
|
||||
if [ -d .git ]; then
|
||||
MV='git mv'
|
||||
else
|
||||
MV=mv
|
||||
fi
|
||||
|
||||
sed -i \
|
||||
-e "s/authentic2_plugin_template/$LOWER_UNDERSCORED/g" \
|
||||
-e "s/authentic2-plugin-template/$PROJECT_NAME/g" \
|
||||
-e "s/A2_TEMPLATE_/A2_$UPPER_UNDERSCORED_/g" \
|
||||
-e "s/Authentic2 Plugin Template/$TITLECASE/g" \
|
||||
setup.py src/*/*.py README COPYING MANIFEST.in
|
||||
$MV src/authentic2_plugin_template/static/authentic2_plugin_template \
|
||||
src/authentic2_plugin_template/static/$LOWER_UNDERSCORED
|
||||
$MV src/authentic2_plugin_template/templates/authentic2_plugin_template \
|
||||
src/authentic2_plugin_template/templates/$LOWER_UNDERSCORED
|
||||
$MV src/authentic2_plugin_template src/$LOWER_UNDERSCORED
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
import subprocess
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
|
||||
def get_version():
|
||||
'''Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0.0- and add the length of the commit log.
|
||||
'''
|
||||
if os.path.exists('VERSION'):
|
||||
with open('VERSION', 'r') as v:
|
||||
return v.read()
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(['git','describe','--dirty','--match=v*'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
return result.split()[0][1:].replace('-', '.')
|
||||
else:
|
||||
return '0.0.0-%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.0'
|
||||
|
||||
README = file(os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'README')).read()
|
||||
|
||||
setup(name='authentic2-plugin-template',
|
||||
version=get_version(),
|
||||
license='AGPLv3',
|
||||
description='Authentic2 Plugin Template',
|
||||
long_description=README,
|
||||
author="Entr'ouvert",
|
||||
author_email="info@entrouvert.com",
|
||||
packages=find_packages('src'),
|
||||
package_dir={
|
||||
'': 'src',
|
||||
},
|
||||
package_data={
|
||||
'authentic2_plugin_template': [
|
||||
'templates/authentic2_plugin_template/*.html',
|
||||
'static/authentic2_plugin_template/js/*.js',
|
||||
'static/authentic2_plugin_template/css/*.css',
|
||||
'static/authentic2_plugin_template/img/*.png',
|
||||
],
|
||||
},
|
||||
install_requires=[
|
||||
],
|
||||
entry_points={
|
||||
'authentic2.plugin': [
|
||||
'authentic2-plugin-template= authentic2_plugin_template:Plugin',
|
||||
],
|
||||
},
|
||||
)
|
|
@ -1,58 +0,0 @@
|
|||
__version__ = '1.0.0'
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_after_urls(self):
|
||||
return []
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def get_before_middleware(self):
|
||||
return []
|
||||
|
||||
def get_after_middleware(self):
|
||||
return []
|
||||
|
||||
def get_authentication_backends(self):
|
||||
return []
|
||||
|
||||
def get_auth_frontends(self):
|
||||
return []
|
||||
|
||||
def get_idp_backends(self):
|
||||
return []
|
||||
|
||||
def service_list(self, request):
|
||||
'''For IdP plugins this method add links to the user homepage.
|
||||
|
||||
It must return a list of authentic2.utils.Service objects, each
|
||||
object has a name and can have an url and some actions.
|
||||
|
||||
Service(name=name[, url=url[, actions=actions]])
|
||||
|
||||
Actions are a list of tuples, whose parts are
|
||||
- first the name of the action,
|
||||
- the HTTP method for calling the action,
|
||||
- the URL for calling the action,
|
||||
- the paramters to pass to this URL as a sequence of key-value tuples.
|
||||
'''
|
||||
return []
|
||||
|
||||
def logout_list(self, request):
|
||||
'''For IdP or SP plugins this method add actions to logout from remote
|
||||
IdP or SP.
|
||||
|
||||
It must returns a list of HTML fragments, each fragment is
|
||||
responsible for calling the view doing the logout. Views are usually
|
||||
called using <img/> or <iframge/> tags and finally redirect to an
|
||||
icon indicating success or failure for the logout.
|
||||
|
||||
Authentic2 provide two such icons through the following URLs:
|
||||
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png')
|
||||
- os.path.join(settings.STATIC_URL, 'authentic2/img/ok.png')
|
||||
'''
|
||||
return []
|
|
@ -1,5 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
# registrer your admin editable models here using admin.register
|
|
@ -1,23 +0,0 @@
|
|||
class AppSettings(object):
|
||||
__DEFAULTS = {
|
||||
'ENABLE': True,
|
||||
}
|
||||
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
|
||||
def _setting(self, name, dflt):
|
||||
from django.conf import settings
|
||||
return getattr(settings, self.prefix+name, dflt)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self.__DEFAULTS:
|
||||
raise AttributeError(name)
|
||||
return self._setting(name, self.__DEFAULTS[name])
|
||||
|
||||
# Ugly? Guido recommends this himself ...
|
||||
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||
import sys
|
||||
app_settings = AppSettings('A2_PLUGIN_TEMPLATE_')
|
||||
app_settings.__name__ = __name__
|
||||
sys.modules[__name__] = app_settings
|
|
@ -1,3 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# put your models here
|
|
@ -1 +0,0 @@
|
|||
{% comment %}placeholder{% endcomment %}
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
from . import app_settings
|
||||
from .views import index
|
||||
|
||||
urlpatterns = required(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[url('^authentic2_plugin_template/$', index, name='authentic2-plugin-template-index')]
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
|
||||
from . import decorators
|
||||
|
||||
__ALL_ = [ 'sso' ]
|
||||
|
||||
@decorators.plugin_enabled
|
||||
def index(request):
|
||||
return render(request, 'authentic2_plugin_template/index.html')
|
12
setup.py
12
setup.py
|
@ -156,14 +156,4 @@ setup(name="authentic2",
|
|||
'compile_translations': compile_translations,
|
||||
'sdist': sdist,
|
||||
},
|
||||
entry_points={
|
||||
'authentic2.plugin': [
|
||||
'authentic2-auth-ssl = authentic2.auth2_auth.auth2_ssl:Plugin',
|
||||
'authentic2-auth-saml = authentic2_auth_saml:Plugin',
|
||||
'authentic2-auth-oidc = authentic2_auth_oidc:Plugin',
|
||||
'authentic2-idp-saml2 = authentic2.idp.saml:Plugin',
|
||||
'authentic2-idp-cas = authentic2_idp_cas:Plugin',
|
||||
'authentic2-idp-oidc = authentic2_idp_oidc:Plugin',
|
||||
'authentic2-provisionning-ldap = authentic2_provisionning_ldap:Plugin',
|
||||
],
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
import re
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.apps import AppConfig, apps
|
||||
from django.views import debug
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from . import plugins
|
||||
|
||||
|
||||
class AppMixin(object):
|
||||
def a2_hook_urls(self):
|
||||
try:
|
||||
return import_string('%s.urls.urlpatterns' % self.name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
class A2AppConfig(AppMixin, AppConfig):
|
||||
pass
|
||||
|
||||
|
||||
class Authentic2Config(AppConfig):
|
||||
name = 'authentic2'
|
||||
verbose_name = 'Authentic2'
|
||||
|
@ -14,3 +27,16 @@ class Authentic2Config(AppConfig):
|
|||
plugins.init()
|
||||
debug.HIDDEN_SETTINGS = re.compile(
|
||||
'API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|LDAP')
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
# apply only to authentic2 applications
|
||||
if not app.name.startswith('authentic2_'):
|
||||
continue
|
||||
if app.__class__ is AppConfig:
|
||||
# switch class if it's an application without a custom
|
||||
# appconfig.
|
||||
app.__class__ = A2AppConfig
|
||||
else:
|
||||
# add mixin to base classes if it's an application with a
|
||||
# custom appconfig.
|
||||
app.__class__.__bases__ = (AppMixin,) + app.__class__.__bases__
|
||||
|
|
|
@ -19,7 +19,7 @@ from django.core.files.storage import default_storage
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .decorators import to_iter
|
||||
from .decorators import to_iter, GlobalCache
|
||||
from .plugins import collect_from_plugins
|
||||
from . import app_settings
|
||||
from .forms import widgets, fields
|
||||
|
@ -231,11 +231,16 @@ DEFAULT_ATTRIBUTE_KINDS = [
|
|||
]
|
||||
|
||||
|
||||
@GlobalCache
|
||||
def get_plugin_attribute_kinds():
|
||||
return [kinds for kind in chain(*collect_from_plugins('attribute_kinds')) if kind]
|
||||
|
||||
|
||||
def get_attribute_kinds():
|
||||
attribute_kinds = {}
|
||||
for attribute_kind in chain(DEFAULT_ATTRIBUTE_KINDS, app_settings.A2_ATTRIBUTE_KINDS):
|
||||
attribute_kinds[attribute_kind['name']] = attribute_kind
|
||||
for attribute_kind in chain(*collect_from_plugins('attribute_kinds')):
|
||||
for attribute_kind in get_plugin_attribute_kinds():
|
||||
attribute_kinds[attribute_kind['name']] = attribute_kind
|
||||
return attribute_kinds
|
||||
|
||||
|
|
|
@ -1,19 +1 @@
|
|||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import app_settings
|
||||
from django.conf.urls import include, url
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
return required(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[
|
||||
url(r'^accounts/sslauth/', include(__name__ + '.urls'))])
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def get_authentication_backends(self):
|
||||
return ['authentic2.auth2_auth.auth2_ssl.backends.SSLBackend']
|
||||
|
||||
def get_auth_frontends(self):
|
||||
return ['authentic2.auth2_auth.auth2_ssl.frontends.SSLFrontend']
|
||||
default_app_config = 'authentic2.auth2_auth.auth2_ssl.apps.AppConfig'
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# authentic2.auth2_auth.auth2_ssl - Authentic2 Auth SSL plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
from authentic2.apps import AppMixin
|
||||
|
||||
|
||||
class AppConfig(AppMixin, django.apps.AppConfig):
|
||||
name = 'authentic2.auth2_auth.auth2_ssl'
|
||||
|
||||
def a2_hook_get_authentication_backends(self):
|
||||
return ['authentic2.auth2_auth.auth2_ssl.backends.SSLBackend']
|
||||
|
||||
def a2_hook_get_auth_frontends(self):
|
||||
return ['authentic2.auth2_auth.auth2_ssl.frontends.SSLFrontend']
|
|
@ -1,6 +1,25 @@
|
|||
from django.conf.urls import url
|
||||
# authentic2.auth2_auth.auth2_ssl - Authentic2 Auth SSL plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
from .views import (handle_request, post_account_linking, delete_certificate,
|
||||
error_ssl)
|
||||
error_ssl)
|
||||
from . import app_settings
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$',
|
||||
|
@ -16,3 +35,10 @@ urlpatterns = [
|
|||
error_ssl,
|
||||
name='error_ssl'),
|
||||
]
|
||||
|
||||
urlpatterns = required(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
[
|
||||
url(r'^accounts/sslauth/', include(urlpatterns)),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import urlparse
|
|||
|
||||
from django.conf import settings
|
||||
|
||||
from . import plugins, app_settings
|
||||
from . import plugins, app_settings, hooks
|
||||
|
||||
|
||||
def make_origin(url):
|
||||
|
@ -40,10 +40,4 @@ def check_origin(request, origin):
|
|||
for whitelist_origin in app_settings.A2_CORS_WHITELIST:
|
||||
if whitelist_origin == origin:
|
||||
return True
|
||||
for plugin in plugins.get_plugins():
|
||||
if hasattr(plugin, 'check_origin'):
|
||||
if plugin.check_origin(request, origin):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
return any(hooks.call_hooks('check_origin', request, origin))
|
||||
|
|
|
@ -1,61 +1 @@
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.checks import register, Warning, Tags
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import app_settings
|
||||
from django.conf.urls import url, include
|
||||
from authentic2.decorators import (setting_enabled, required,
|
||||
lasso_required)
|
||||
|
||||
return required(
|
||||
(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
lasso_required()
|
||||
),
|
||||
[url(r'^idp/saml2/', include(__name__ + '.urls'))])
|
||||
|
||||
def get_apps(self):
|
||||
return ['authentic2.idp.saml']
|
||||
|
||||
def get_idp_backends(self):
|
||||
return ['authentic2.idp.saml.backend.SamlBackend']
|
||||
|
||||
def check_origin(self, request, origin):
|
||||
from authentic2.cors import make_origin
|
||||
from authentic2.saml.models import LibertySession
|
||||
for session in LibertySession.objects.filter(
|
||||
django_session_key=request.session.session_key):
|
||||
provider_origin = make_origin(session.provider_id)
|
||||
if origin == provider_origin:
|
||||
return True
|
||||
|
||||
|
||||
class SAML2IdPConfig(AppConfig):
|
||||
name = 'authentic2.idp.saml'
|
||||
label = 'authentic2_idp_saml'
|
||||
default_app_config = 'authentic2.idp.saml.SAML2IdPConfig'
|
||||
|
||||
|
||||
def check_authentic2_config(app_configs, **kwargs):
|
||||
from . import app_settings
|
||||
errors = []
|
||||
|
||||
if not settings.DEBUG and app_settings.ENABLE and \
|
||||
(app_settings.is_default('SIGNATURE_PUBLIC_KEY') or
|
||||
app_settings.is_default('SIGNATURE_PRIVATE_KEY')):
|
||||
errors.append(
|
||||
Warning(
|
||||
'You should not use default SAML keys in production',
|
||||
hint='Generate new RSA keys and change the value of '
|
||||
'A2_IDP_SAML2_SIGNATURE_PUBLIC_KEY and '
|
||||
'A2_IDP_SAML2_SIGNATURE_PRIVATE_KEY in your setting file',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
check_authentic2_config = register(Tags.security,
|
||||
deploy=True)(check_authentic2_config)
|
||||
default_app_config = 'authentic2.idp.saml.apps.AppConfig'
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# authentic2.idp.saml - Authentic2 SAML IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
from authentic2.apps import AppMixin
|
||||
|
||||
|
||||
class AppConfig(AppMixin, django.apps.AppConfig):
|
||||
name = 'authentic2.idp.saml'
|
||||
label = 'idp_saml'
|
||||
|
||||
def a2_hook_get_idp_backends(self):
|
||||
return ['authentic2.idp.saml.backend.SamlBackend']
|
||||
|
||||
def a2_hooks_check_origin(self, request, origin):
|
||||
from authentic2.cors import make_origin
|
||||
from authentic2.saml.models import LibertySession
|
||||
|
||||
for session in LibertySession.objects.filter(
|
||||
django_session_key=request.session.session_key):
|
||||
provider_origin = make_origin(session.provider_id)
|
||||
if origin == provider_origin:
|
||||
return True
|
||||
|
||||
def ready(self):
|
||||
from django.core.checks import register, Tags
|
||||
|
||||
register(check_authentic2_config, Tags.security, deploy=True)
|
||||
|
||||
|
||||
def check_authentic2_config(app_configs, **kwargs):
|
||||
from . import app_settings
|
||||
from django.conf import settings
|
||||
from django.core.checks import Warning
|
||||
|
||||
errors = []
|
||||
|
||||
if not settings.DEBUG and app_settings.ENABLE and \
|
||||
(app_settings.is_default('SIGNATURE_PUBLIC_KEY') or
|
||||
app_settings.is_default('SIGNATURE_PRIVATE_KEY')):
|
||||
errors.append(
|
||||
Warning(
|
||||
'You should not use default SAML keys in production',
|
||||
hint='Generate new RSA keys and change the value of '
|
||||
'A2_IDP_SAML2_SIGNATURE_PUBLIC_KEY and '
|
||||
'A2_IDP_SAML2_SIGNATURE_PRIVATE_KEY in your setting file',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
|
@ -1,10 +1,27 @@
|
|||
from django.conf.urls import url
|
||||
# authentic2.idp.saml Authentic2 SAML IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from . import views
|
||||
from authentic2.idp.saml.saml2_endpoints import (metadata, sso, continue_sso,
|
||||
slo, slo_soap, idp_slo,
|
||||
slo_return, finish_slo,
|
||||
artifact, idp_sso)
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from authentic2.decorators import setting_enabled, required, lasso_required
|
||||
|
||||
from . import views, app_settings
|
||||
from .saml2_endpoints import (metadata, sso, continue_sso, slo, slo_soap,
|
||||
idp_slo, slo_return, finish_slo, artifact,
|
||||
idp_sso)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^metadata$', metadata, name='a2-idp-saml-metadata'),
|
||||
|
@ -29,3 +46,7 @@ urlpatterns = [
|
|||
views.delete_federation,
|
||||
name='a2-idp-saml2-federation-delete'),
|
||||
]
|
||||
urlpatterns = required(
|
||||
[setting_enabled('ENABLE', settings=app_settings),
|
||||
lasso_required()],
|
||||
[url(r'^idp/saml2/', include(urlpatterns))])
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Use setuptools entrypoints to find plugins
|
||||
|
||||
|
@ -6,42 +22,35 @@
|
|||
import pkg_resources
|
||||
import logging
|
||||
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
__ALL__ = ['get_plugins']
|
||||
|
||||
PLUGIN_CACHE = {}
|
||||
|
||||
|
||||
class PluginError(Exception):
|
||||
pass
|
||||
|
||||
DEFAULT_GROUP_NAME = 'authentic2.plugin'
|
||||
|
||||
def get_plugins(group_name=DEFAULT_GROUP_NAME, use_cache=True, *args, **kwargs):
|
||||
|
||||
def get_plugins(group_name=DEFAULT_GROUP_NAME, *args, **kwargs):
|
||||
'''Traverse all entry points for group_name and instantiate them using args
|
||||
and kwargs.
|
||||
'''
|
||||
global PLUGIN_CACHE
|
||||
if group_name in PLUGIN_CACHE and use_cache:
|
||||
return PLUGIN_CACHE[group_name]
|
||||
plugins = []
|
||||
for entrypoint in pkg_resources.iter_entry_points(group_name):
|
||||
try:
|
||||
plugin_callable = entrypoint.load()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception('unable to load entrypoint %s', entrypoint)
|
||||
raise PluginError('unable to load entrypoint %s' % entrypoint, e)
|
||||
plugins.append(plugin_callable(*args, **kwargs))
|
||||
PLUGIN_CACHE[group_name] = plugins
|
||||
return plugins
|
||||
yield plugin_callable(*args, **kwargs)
|
||||
|
||||
def register_plugins_urls(urlpatterns,
|
||||
group_name=DEFAULT_GROUP_NAME):
|
||||
|
||||
def register_urls(urlpatterns):
|
||||
'''Call get_before_urls and get_after_urls on all plugins providing them
|
||||
and add those urls to the given urlpatterns.
|
||||
|
||||
|
@ -49,7 +58,9 @@ def register_plugins_urls(urlpatterns,
|
|||
and those returned by get_after_urls() are added to the tail of
|
||||
urlpatterns.
|
||||
'''
|
||||
plugins = get_plugins(group_name)
|
||||
from .hooks import call_hooks
|
||||
|
||||
plugins = get_plugins()
|
||||
before_urls = []
|
||||
after_urls = []
|
||||
for plugin in plugins:
|
||||
|
@ -59,14 +70,17 @@ def register_plugins_urls(urlpatterns,
|
|||
if hasattr(plugin, 'get_after_urls'):
|
||||
urls = plugin.get_after_urls()
|
||||
after_urls.append(url('^', include(urls)))
|
||||
|
||||
# use new hook
|
||||
for urls in call_hooks('urls'):
|
||||
if urls:
|
||||
before_urls.append(url('^', include(urls)))
|
||||
return before_urls + urlpatterns + after_urls
|
||||
|
||||
|
||||
def register_plugins_installed_apps(installed_apps, group_name=DEFAULT_GROUP_NAME):
|
||||
'''Call get_apps() on all plugins of group_name and add the returned
|
||||
applications path to the installed_apps sequence.
|
||||
|
||||
Applications already present are ignored.
|
||||
applications path to the installed_apps sequence. Applications already
|
||||
present are ignored.
|
||||
'''
|
||||
installed_apps = list(installed_apps)
|
||||
for plugin in get_plugins(group_name):
|
||||
|
@ -77,73 +91,44 @@ def register_plugins_installed_apps(installed_apps, group_name=DEFAULT_GROUP_NAM
|
|||
installed_apps.append(app)
|
||||
return installed_apps
|
||||
|
||||
def register_plugins_middleware(middleware_classes,
|
||||
group_name=DEFAULT_GROUP_NAME):
|
||||
middleware_classes = list(middleware_classes)
|
||||
for plugin in get_plugins(group_name):
|
||||
if hasattr(plugin, 'get_before_middleware'):
|
||||
apps = plugin.get_before_middleware()
|
||||
for app in reversed(apps):
|
||||
if app not in middleware_classes:
|
||||
middleware_classes.insert(0, app)
|
||||
if hasattr(plugin, 'get_after_middleware'):
|
||||
apps = plugin.get_after_middleware()
|
||||
for app in apps:
|
||||
if app not in middleware_classes:
|
||||
middleware_classes.append(app)
|
||||
return tuple(middleware_classes)
|
||||
|
||||
def register_plugins_authentication_backends(authentication_backends,
|
||||
group_name=DEFAULT_GROUP_NAME):
|
||||
authentication_backends = list(authentication_backends)
|
||||
for plugin in get_plugins(group_name):
|
||||
if hasattr(plugin, 'get_authentication_backends'):
|
||||
cls = plugin.get_authentication_backends()
|
||||
for cls in cls:
|
||||
if cls not in authentication_backends:
|
||||
authentication_backends.append(cls)
|
||||
return tuple(authentication_backends)
|
||||
def register_classes(what, initial=[]):
|
||||
accum = list(initial)
|
||||
|
||||
def register_plugins_auth_frontends(auth_frontends=(),
|
||||
group_name=DEFAULT_GROUP_NAME):
|
||||
auth_frontends = list(auth_frontends)
|
||||
for plugin in get_plugins(group_name):
|
||||
if hasattr(plugin, 'get_auth_frontends'):
|
||||
cls = plugin.get_auth_frontends()
|
||||
for cls in cls:
|
||||
if cls not in auth_frontends:
|
||||
auth_frontends.append(cls)
|
||||
return tuple(auth_frontends)
|
||||
|
||||
def register_plugins_idp_backends(idp_backends,
|
||||
group_name=DEFAULT_GROUP_NAME):
|
||||
idp_backends = list(idp_backends)
|
||||
for plugin in get_plugins(group_name):
|
||||
if hasattr(plugin, 'get_idp_backends'):
|
||||
cls = plugin.get_idp_backends()
|
||||
for cls in cls:
|
||||
if cls not in idp_backends:
|
||||
idp_backends.append(cls)
|
||||
return tuple(idp_backends)
|
||||
for klasses in collect_from_plugins(what):
|
||||
for klass in klasses:
|
||||
if klass not in accum:
|
||||
accum.append(klass)
|
||||
return accum
|
||||
|
||||
|
||||
def collect_from_plugins(name, *args, **kwargs):
|
||||
'''
|
||||
Collect a property or the result of a function from plugins.
|
||||
Collect a property or the result of a function from plugins and hooks.
|
||||
'''
|
||||
accumulator = []
|
||||
from .hooks import call_hooks
|
||||
|
||||
for plugin in get_plugins():
|
||||
if not hasattr(plugin, name):
|
||||
continue
|
||||
attribute = getattr(plugin, name)
|
||||
if hasattr(attribute, '__call__'):
|
||||
accumulator.append(attribute(*args, **kwargs))
|
||||
yield attribute(*args, **kwargs)
|
||||
else:
|
||||
accumulator.append(attribute)
|
||||
return accumulator
|
||||
yield attribute
|
||||
|
||||
for value in call_hooks(name, *args, **kwargs):
|
||||
if value:
|
||||
yield value
|
||||
|
||||
|
||||
def init():
|
||||
from django.conf import settings
|
||||
|
||||
for plugin in get_plugins():
|
||||
if hasattr(plugin, 'init'):
|
||||
plugin.init()
|
||||
|
||||
settings.AUTHENTICATION_BACKENDS = register_classes('get_authentication_backends', settings.AUTHENTICATION_BACKENDS)
|
||||
settings.AUTH_FRONTENDS = register_classes('get_auth_frontends', settings.AUTH_FRONTENDS)
|
||||
settings.IDP_BACKENDS = register_classes('get_idp_backends', settings.IDP_BACKENDS)
|
||||
|
|
|
@ -85,11 +85,6 @@ MIDDLEWARE_CLASSES = (
|
|||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
DATABASES['default']['ATOMIC_REQUESTS'] = True
|
||||
|
||||
MIDDLEWARE_CLASSES += (
|
||||
'authentic2.middleware.DisplayMessageBeforeRedirectMiddleware',
|
||||
'authentic2.idp.middleware.DebugMiddleware',
|
||||
'authentic2.middleware.CollectIPMiddleware',
|
||||
|
@ -97,7 +92,7 @@ MIDDLEWARE_CLASSES += (
|
|||
'authentic2.middleware.OpenedSessionCookieMiddleware',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = plugins.register_plugins_middleware(MIDDLEWARE_CLASSES)
|
||||
DATABASES['default']['ATOMIC_REQUESTS'] = True
|
||||
|
||||
ROOT_URLCONF = 'authentic2.urls'
|
||||
|
||||
|
@ -126,6 +121,12 @@ INSTALLED_APPS = (
|
|||
'authentic2.disco_service',
|
||||
'authentic2.manager',
|
||||
'authentic2_provisionning_ldap',
|
||||
'authentic2_idp_cas',
|
||||
'authentic2_idp_oidc',
|
||||
'authentic2_auth_oidc',
|
||||
'mellon',
|
||||
'authentic2_auth_saml',
|
||||
'authentic2.auth2_auth.auth2_ssl',
|
||||
'authentic2',
|
||||
'django_rbac',
|
||||
'authentic2.a2_rbac',
|
||||
|
@ -146,8 +147,6 @@ AUTHENTICATION_BACKENDS = (
|
|||
'authentic2.backends.models_backend.DummyModelBackend',
|
||||
'django_rbac.backends.DjangoRBACBackend',
|
||||
)
|
||||
AUTHENTICATION_BACKENDS = plugins.register_plugins_authentication_backends(
|
||||
AUTHENTICATION_BACKENDS)
|
||||
CSRF_FAILURE_VIEW = 'authentic2.views.csrf_failure_view'
|
||||
|
||||
|
||||
|
@ -164,8 +163,7 @@ ACCOUNT_ACTIVATION_DAYS = 2
|
|||
# Authentication settings
|
||||
###########################
|
||||
AUTH_USER_MODEL = 'custom_user.User'
|
||||
AUTH_FRONTENDS = plugins.register_plugins_auth_frontends((
|
||||
'authentic2.auth_frontends.LoginPasswordBackend',))
|
||||
AUTH_FRONTENDS = ['authentic2.auth_frontends.LoginPasswordBackend']
|
||||
|
||||
###########################
|
||||
# RBAC settings
|
||||
|
@ -181,7 +179,7 @@ RBAC_ROLE_PARENTING_MODEL = 'a2_rbac.RoleParenting'
|
|||
|
||||
# List of IdP backends, mainly used to show available services in the homepage
|
||||
# of user, and to handle SLO for each protocols
|
||||
IDP_BACKENDS = plugins.register_plugins_idp_backends(())
|
||||
IDP_BACKENDS = []
|
||||
|
||||
# Whether to autoload SAML 2.0 identity providers and services metadata
|
||||
# Only https URLS are accepted.
|
||||
|
|
|
@ -56,4 +56,4 @@ if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
|
|||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
||||
urlpatterns = plugins.register_plugins_urls(urlpatterns)
|
||||
urlpatterns = plugins.register_urls(urlpatterns)
|
||||
|
|
|
@ -48,7 +48,7 @@ except ImportError:
|
|||
from authentic2.saml.saml2utils import filter_attribute_private_key, \
|
||||
filter_element_private_key
|
||||
|
||||
from . import plugins, app_settings, constants
|
||||
from . import app_settings, constants
|
||||
|
||||
|
||||
class CleanLogMessage(logging.Filter):
|
||||
|
@ -119,6 +119,8 @@ class IterableFactory(object):
|
|||
|
||||
|
||||
def accumulate_from_backends(request, method_name, **kwargs):
|
||||
from . import plugins
|
||||
|
||||
list = []
|
||||
for backend in get_backends():
|
||||
method = getattr(backend, method_name, None)
|
||||
|
|
|
@ -511,13 +511,24 @@ class ProfileView(cbv.TemplateNamesMixin, TemplateView):
|
|||
|
||||
profile = login_required(ProfileView.as_view())
|
||||
|
||||
|
||||
def logout_list(request):
|
||||
'''Return logout links from idp backends'''
|
||||
return utils.accumulate_from_backends(request, 'logout_list')
|
||||
accum = utils.accumulate_from_backends(request, 'logout_list')
|
||||
for result in hooks.call_hooks('logout_list', request):
|
||||
if result:
|
||||
accum.extend(result)
|
||||
return accum
|
||||
|
||||
|
||||
def redirect_logout_list(request):
|
||||
'''Return redirect logout links from idp backends'''
|
||||
return utils.accumulate_from_backends(request, 'redirect_logout_list')
|
||||
accum = utils.accumulate_from_backends(request, 'redirect_logout_list')
|
||||
for result in hooks.call_hooks('redirect_logout_list', request):
|
||||
if result:
|
||||
accum.extend(result)
|
||||
return accum
|
||||
|
||||
|
||||
def logout(request, next_url=None, default_next_url='auth_homepage',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
|
|
|
@ -1,68 +1,17 @@
|
|||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from authentic2.utils import make_url
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def get_authentication_backends(self):
|
||||
return ['authentic2_auth_oidc.backends.OIDCBackend']
|
||||
|
||||
def get_auth_frontends(self):
|
||||
return ['authentic2_auth_oidc.auth_frontends.OIDCFrontend']
|
||||
|
||||
def redirect_logout_list(self, request, next=None):
|
||||
from .models import OIDCProvider
|
||||
|
||||
tokens = request.session.get('auth_oidc', {}).get('tokens', [])
|
||||
urls = []
|
||||
if tokens:
|
||||
for token in tokens:
|
||||
provider = OIDCProvider.objects.get(pk=token['provider_pk'])
|
||||
# ignore providers wihtout SLO
|
||||
if not provider.end_session_endpoint:
|
||||
continue
|
||||
params = {}
|
||||
if 'id_token' in token['token_response']:
|
||||
params['id_token_hint'] = token['token_response']['id_token']
|
||||
if 'access_token' in token['token_response'] and provider.token_revocation_endpoint:
|
||||
self.revoke_token(provider, token['token_response']['access_token'])
|
||||
params['post_logout_redirect_uri'] = request.build_absolute_uri(reverse('auth_logout'))
|
||||
urls.append(make_url(provider.end_session_endpoint, params=params))
|
||||
return urls
|
||||
|
||||
def revoke_token(self, provider, access_token):
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
url = provider.token_revocation_endpoint
|
||||
try:
|
||||
response = requests.post(url, auth=(provider.client_id, provider.client_secret),
|
||||
data={'token': access_token, 'token_type': 'access_token'},
|
||||
timeout=10)
|
||||
except requests.RequestException as e:
|
||||
logger.warning(u'failed to revoke access token from OIDC provider %s: %s',
|
||||
provider.issuer, e)
|
||||
return
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
try:
|
||||
content = response.json()
|
||||
except ValueError:
|
||||
content = None
|
||||
logger.warning(u'failed to revoke access token from OIDC provider %s: %s, %s',
|
||||
provider.issuer, e, content)
|
||||
return
|
||||
logger.info(u'revoked token from OIDC provider %s', provider.issuer)
|
||||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
default_app_config = 'authentic2_auth_oidc.apps.AppConfig'
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'authentic2_auth_oidc'
|
||||
|
||||
def a2_hook_get_authentication_backends(self):
|
||||
return ['authentic2_auth_oidc.backends.OIDCBackend']
|
||||
|
||||
def a2_hook_get_auth_frontends(self):
|
||||
return ['authentic2_auth_oidc.auth_frontends.OIDCFrontend']
|
||||
|
||||
def a2_hook_redirect_logout_list(self, request, next=None):
|
||||
from .models import OIDCProvider
|
||||
from authentic2.utils import make_url
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
tokens = request.session.get('auth_oidc', {}).get('tokens', [])
|
||||
urls = []
|
||||
if tokens:
|
||||
for token in tokens:
|
||||
provider = OIDCProvider.objects.get(pk=token['provider_pk'])
|
||||
# ignore providers wihtout SLO
|
||||
if not provider.end_session_endpoint:
|
||||
continue
|
||||
params = {}
|
||||
if 'id_token' in token['token_response']:
|
||||
params['id_token_hint'] = token['token_response']['id_token']
|
||||
if 'access_token' in token['token_response'] and provider.token_revocation_endpoint:
|
||||
self.revoke_token(provider, token['token_response']['access_token'])
|
||||
params['post_logout_redirect_uri'] = request.build_absolute_uri(reverse('auth_logout'))
|
||||
urls.append(make_url(provider.end_session_endpoint, params=params))
|
||||
return urls
|
||||
|
||||
def revoke_token(self, provider, access_token):
|
||||
import requests
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
url = provider.token_revocation_endpoint
|
||||
try:
|
||||
response = requests.post(url, auth=(provider.client_id, provider.client_secret),
|
||||
data={'token': access_token, 'token_type': 'access_token'},
|
||||
timeout=10)
|
||||
except requests.RequestException as e:
|
||||
logger.warning(u'failed to revoke access token from OIDC provider %s: %s',
|
||||
provider.issuer, e)
|
||||
return
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
try:
|
||||
content = response.json()
|
||||
except ValueError:
|
||||
content = None
|
||||
logger.warning(u'failed to revoke access token from OIDC provider %s: %s, %s',
|
||||
provider.issuer, e, content)
|
||||
return
|
||||
logger.info(u'revoked token from OIDC provider %s', provider.issuer)
|
||||
|
|
@ -1,21 +1 @@
|
|||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_apps(self):
|
||||
return ['mellon', __name__]
|
||||
|
||||
def get_authentication_backends(self):
|
||||
return ['authentic2_auth_saml.backends.SAMLBackend']
|
||||
|
||||
def get_auth_frontends(self):
|
||||
return ['authentic2_auth_saml.auth_frontends.SAMLFrontend']
|
||||
|
||||
def redirect_logout_list(self, request, next_url=None):
|
||||
from mellon.views import logout
|
||||
if 'mellon_session' in request.session:
|
||||
response = logout(request)
|
||||
if 'Location' in response:
|
||||
return [response['Location']]
|
||||
return []
|
||||
default_app_config = 'authentic2_auth_saml.apps.AppConfig'
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'authentic2_auth_saml'
|
||||
|
||||
def a2_hook_get_authentication_backends(self):
|
||||
return ['authentic2_auth_saml.backends.SAMLBackend']
|
||||
|
||||
def a2_hook_get_auth_frontends(self):
|
||||
return ['authentic2_auth_saml.auth_frontends.SAMLFrontend']
|
||||
|
||||
def a2_hook_redirect_logout_list(self, request, next_url=None):
|
||||
from mellon.views import logout
|
||||
if 'mellon_session' in request.session:
|
||||
response = logout(request)
|
||||
if 'Location' in response:
|
||||
return [response['Location']]
|
||||
return []
|
||||
|
|
@ -1,33 +1 @@
|
|||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .constants import SESSION_CAS_LOGOUTS
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import app_settings
|
||||
from django.conf.urls import include, url
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
return required(
|
||||
(
|
||||
setting_enabled('ENABLE', settings=app_settings),
|
||||
),
|
||||
[url(r'^idp/cas/', include(__name__ + '.urls'))])
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def logout_list(self, request):
|
||||
fragments = []
|
||||
cas_logouts = request.session.get(SESSION_CAS_LOGOUTS, [])
|
||||
for name, url, use_iframe, use_iframe_timeout in cas_logouts:
|
||||
ctx = {
|
||||
'needs_iframe': use_iframe,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'iframe_timeout': use_iframe_timeout,
|
||||
}
|
||||
content = render_to_string('authentic2_idp_cas/logout_fragment.html', ctx)
|
||||
fragments.append(content)
|
||||
return fragments
|
||||
default_app_config = 'authentic2_idp_cas.apps.AppConfig'
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'authentic2_idp_cas'
|
||||
|
||||
def a2_hook_logout_list(self, request):
|
||||
from .constants import SESSION_CAS_LOGOUTS
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
fragments = []
|
||||
cas_logouts = request.session.get(SESSION_CAS_LOGOUTS, [])
|
||||
for name, url, use_iframe, use_iframe_timeout in cas_logouts:
|
||||
ctx = {
|
||||
'needs_iframe': use_iframe,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'iframe_timeout': use_iframe_timeout,
|
||||
}
|
||||
content = render_to_string('authentic2_idp_cas/logout_fragment.html', ctx)
|
||||
fragments.append(content)
|
||||
return fragments
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
from django.conf.urls import url
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from . import views
|
||||
from authentic2.decorators import setting_enabled, required
|
||||
|
||||
from . import views, app_settings
|
||||
|
||||
urlpatterns = [
|
||||
url('^login/?$', views.login, name='a2-idp-cas-login'),
|
||||
|
@ -13,3 +15,9 @@ urlpatterns = [
|
|||
url('^proxyValidate/?$', views.proxy_validate,
|
||||
name='a2-idp-cas-proxy-validate'),
|
||||
]
|
||||
urlpatterns = required(
|
||||
[setting_enabled('ENABLE', settings=app_settings)],
|
||||
[
|
||||
url(r'^idp/cas/', include(urlpatterns)),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,34 +1 @@
|
|||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
default_app_config = 'authentic2_idp_oidc.apps.AppConfig'
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
||||
|
||||
def logout_list(self, request):
|
||||
from .utils import get_oidc_sessions
|
||||
from . import app_settings
|
||||
|
||||
fragments = []
|
||||
|
||||
oidc_sessions = get_oidc_sessions(request)
|
||||
for key, value in oidc_sessions.iteritems():
|
||||
if 'frontchannel_logout_uri' not in value:
|
||||
continue
|
||||
ctx = {
|
||||
'url': value['frontchannel_logout_uri'],
|
||||
'name': value['name'],
|
||||
'iframe_timeout': value.get('frontchannel_timeout') or app_settings.DEFAULT_FRONTCHANNEL_TIMEOUT,
|
||||
}
|
||||
fragments.append(
|
||||
render_to_string(
|
||||
'authentic2_idp_oidc/logout_fragment.html',
|
||||
ctx))
|
||||
return fragments
|
|
@ -1,5 +1,5 @@
|
|||
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
# Copyright (C) 2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
|
@ -17,94 +17,114 @@
|
|||
import django.apps
|
||||
from django.utils.encoding import smart_bytes
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'authentic2_idp_oidc'
|
||||
name = 'authentic2_idp_oidc'
|
||||
|
||||
# implement translation of encrypted pairwise identifiers when and OIDC Client is using the
|
||||
# A2 API
|
||||
def a2_hook_api_modify_serializer(self, view, serializer):
|
||||
from . import utils
|
||||
from rest_framework import serializers
|
||||
# implement translation of encrypted pairwise identifiers when and OIDC Client is using the
|
||||
# A2 API
|
||||
def a2_hook_api_modify_serializer(self, view, serializer):
|
||||
from . import utils
|
||||
from rest_framework import serializers
|
||||
|
||||
if hasattr(view.request.user, 'oidc_client'):
|
||||
client = view.request.user.oidc_client
|
||||
if client.identifier_policy == client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
if hasattr(view.request.user, 'oidc_client'):
|
||||
client = view.request.user.oidc_client
|
||||
if client.identifier_policy == client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
|
||||
def get_oidc_uuuid(user):
|
||||
return utils.make_pairwise_reversible_sub(client, user)
|
||||
serializer.get_oidc_uuid = get_oidc_uuuid
|
||||
serializer.fields['uuid'] = serializers.SerializerMethodField(
|
||||
method_name='get_oidc_uuid')
|
||||
def get_oidc_uuuid(user):
|
||||
return utils.make_pairwise_reversible_sub(client, user)
|
||||
serializer.get_oidc_uuid = get_oidc_uuuid
|
||||
serializer.fields['uuid'] = serializers.SerializerMethodField(
|
||||
method_name='get_oidc_uuid')
|
||||
|
||||
def a2_hook_api_modify_view_before_get_object(self, view):
|
||||
'''Decrypt sub used as pk argument in URL.'''
|
||||
import uuid
|
||||
from . import utils
|
||||
def a2_hook_api_modify_view_before_get_object(self, view):
|
||||
'''Decrypt sub used as pk argument in URL.'''
|
||||
import uuid
|
||||
from . import utils
|
||||
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
client = request.user.oidc_client
|
||||
if client.identifier_policy != client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
return
|
||||
lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
|
||||
if lookup_url_kwarg not in view.kwargs:
|
||||
return
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
client = request.user.oidc_client
|
||||
if client.identifier_policy != client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
return
|
||||
lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
|
||||
if lookup_url_kwarg not in view.kwargs:
|
||||
return
|
||||
|
||||
sub = smart_bytes(view.kwargs[lookup_url_kwarg])
|
||||
decrypted = utils.reverse_pairwise_sub(client, sub)
|
||||
sub = smart_bytes(view.kwargs[lookup_url_kwarg])
|
||||
decrypted = utils.reverse_pairwise_sub(client, sub)
|
||||
if decrypted:
|
||||
view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted).hex
|
||||
|
||||
def a2_hook_api_modify_serializer_after_validation(self, view, serializer):
|
||||
import uuid
|
||||
from . import utils
|
||||
|
||||
if view.__class__.__name__ != 'UsersAPI':
|
||||
return
|
||||
if serializer.__class__.__name__ != 'SynchronizationSerializer':
|
||||
return
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
client = request.user.oidc_client
|
||||
if client.identifier_policy != client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
return
|
||||
new_known_uuids = []
|
||||
uuid_map = request.uuid_map = {}
|
||||
request.unknown_uuids = []
|
||||
for u in serializer.validated_data['known_uuids']:
|
||||
decrypted = utils.reverse_pairwise_sub(client, smart_bytes(u))
|
||||
if decrypted:
|
||||
view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted).hex
|
||||
new_known_uuid = uuid.UUID(bytes=decrypted).hex
|
||||
new_known_uuids.append(new_known_uuid)
|
||||
uuid_map[new_known_uuid] = u
|
||||
else:
|
||||
request.unknown_uuids.append(u)
|
||||
# undecipherable sub are just not checked at all
|
||||
serializer.validated_data['known_uuids'] = new_known_uuids
|
||||
|
||||
def a2_hook_api_modify_serializer_after_validation(self, view, serializer):
|
||||
import uuid
|
||||
from . import utils
|
||||
def a2_hook_api_modify_response(self, view, method_name, data):
|
||||
'''Reverse mapping applied in a2_hook_api_modify_serializer_after_validation using the
|
||||
uuid_map saved on the request.
|
||||
'''
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
if view.__class__.__name__ != 'UsersAPI':
|
||||
return
|
||||
if method_name != 'synchronization':
|
||||
return
|
||||
if not hasattr(request, 'uuid_map'):
|
||||
return
|
||||
uuid_map = request.uuid_map
|
||||
|
||||
if view.__class__.__name__ != 'UsersAPI':
|
||||
return
|
||||
if serializer.__class__.__name__ != 'SynchronizationSerializer':
|
||||
return
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
client = request.user.oidc_client
|
||||
if client.identifier_policy != client.POLICY_PAIRWISE_REVERSIBLE:
|
||||
return
|
||||
new_known_uuids = []
|
||||
uuid_map = request.uuid_map = {}
|
||||
request.unknown_uuids = []
|
||||
for u in serializer.validated_data['known_uuids']:
|
||||
decrypted = utils.reverse_pairwise_sub(client, smart_bytes(u))
|
||||
if decrypted:
|
||||
new_known_uuid = uuid.UUID(bytes=decrypted).hex
|
||||
new_known_uuids.append(new_known_uuid)
|
||||
uuid_map[new_known_uuid] = u
|
||||
else:
|
||||
request.unknown_uuids.append(u)
|
||||
# undecipherable sub are just not checked at all
|
||||
serializer.validated_data['known_uuids'] = new_known_uuids
|
||||
unknown_uuids = data['unknown_uuids']
|
||||
new_unknown_uuids = []
|
||||
for u in unknown_uuids:
|
||||
new_unknown_uuids.append(uuid_map[u])
|
||||
new_unknown_uuids.extend(request.unknown_uuids)
|
||||
data['unknown_uuids'] = new_unknown_uuids
|
||||
|
||||
def a2_hook_api_modify_response(self, view, method_name, data):
|
||||
'''Reverse mapping applied in a2_hook_api_modify_serializer_after_validation using the
|
||||
uuid_map saved on the request.
|
||||
'''
|
||||
request = view.request
|
||||
if not hasattr(request.user, 'oidc_client'):
|
||||
return
|
||||
if view.__class__.__name__ != 'UsersAPI':
|
||||
return
|
||||
if method_name != 'synchronization':
|
||||
return
|
||||
if not hasattr(request, 'uuid_map'):
|
||||
return
|
||||
uuid_map = request.uuid_map
|
||||
def a2_hook_logout_list(self, request):
|
||||
from .utils import get_oidc_sessions
|
||||
from django.template.loader import render_to_string
|
||||
from . import app_settings
|
||||
|
||||
unknown_uuids = data['unknown_uuids']
|
||||
new_unknown_uuids = []
|
||||
for u in unknown_uuids:
|
||||
new_unknown_uuids.append(uuid_map[u])
|
||||
new_unknown_uuids.extend(request.unknown_uuids)
|
||||
data['unknown_uuids'] = new_unknown_uuids
|
||||
fragments = []
|
||||
|
||||
oidc_sessions = get_oidc_sessions(request)
|
||||
for key, value in oidc_sessions.iteritems():
|
||||
if 'frontchannel_logout_uri' not in value:
|
||||
continue
|
||||
ctx = {
|
||||
'url': value['frontchannel_logout_uri'],
|
||||
'name': value['name'],
|
||||
'iframe_timeout': value.get('frontchannel_timeout') or app_settings.DEFAULT_FRONTCHANNEL_TIMEOUT,
|
||||
}
|
||||
fragments.append(
|
||||
render_to_string(
|
||||
'authentic2_idp_oidc/logout_fragment.html',
|
||||
ctx))
|
||||
return fragments
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
class Plugin(object):
|
||||
def get_apps(self):
|
||||
return [__name__]
|
Loading…
Reference in New Issue