Projet

Général

Profil

0001-spring-cleaning-32934.patch

Benjamin Dauvergne, 13 mai 2019 19:12

Télécharger (691 ko)

Voir les différences:

Subject: [PATCH] spring cleaning (#32934)

* reorganize views and forms
* add copyright headers to all .py files
* fix all style errors reported by flake8
 src/authentic2/__init__.py                    |  16 +
 src/authentic2/a2_rbac/__init__.py            |  16 +
 src/authentic2/a2_rbac/admin.py               |  16 +
 src/authentic2/a2_rbac/app_settings.py        |  20 +-
 src/authentic2/a2_rbac/apps.py                |  20 +-
 src/authentic2/a2_rbac/fields.py              |  17 +
 src/authentic2/a2_rbac/management.py          |  22 +-
 src/authentic2/a2_rbac/managers.py            |  19 +-
 src/authentic2/a2_rbac/models.py              |  36 +-
 src/authentic2/a2_rbac/signal_handlers.py     |  30 +-
 src/authentic2/a2_rbac/tests.py               |  18 +-
 src/authentic2/a2_rbac/utils.py               |  16 +
 src/authentic2/admin.py                       |  83 +-
 src/authentic2/api_urls.py                    |  35 +-
 src/authentic2/api_views.py                   |   9 +-
 src/authentic2/app_settings.py                | 311 +++++---
 src/authentic2/apps.py                        |  16 +-
 src/authentic2/attribute_aggregator/models.py |   0
 src/authentic2/attribute_kinds.py             |  30 +-
 src/authentic2/attributes_ng/engine.py        |  32 +-
 .../attributes_ng/sources/__init__.py         |  16 +
 .../sources/computed_targeted_id.py           |  32 +-
 .../attributes_ng/sources/django_user.py      |  18 +-
 .../attributes_ng/sources/format.py           |  35 +-
 .../attributes_ng/sources/function.py         |  33 +-
 src/authentic2/attributes_ng/sources/ldap.py  |  20 +
 .../attributes_ng/sources/service_roles.py    |  16 +
 .../auth2_auth/auth2_ssl/__init__.py          |  22 +-
 src/authentic2/auth2_auth/auth2_ssl/admin.py  |  17 +
 .../auth2_auth/auth2_ssl/app_settings.py      |  38 +-
 .../auth2_auth/auth2_ssl/authenticators.py    |  16 +
 .../auth2_auth/auth2_ssl/backends.py          |  22 +-
 .../auth2_auth/auth2_ssl/middleware.py        |  19 +-
 src/authentic2/auth2_auth/auth2_ssl/models.py |  17 +
 src/authentic2/auth2_auth/auth2_ssl/urls.py   |  19 +-
 src/authentic2/auth2_auth/auth2_ssl/util.py   |  28 +-
 src/authentic2/auth2_auth/auth2_ssl/views.py  |  77 +-
 src/authentic2/auth2_auth/models.py           |   0
 src/authentic2/authentication.py              |  16 +
 src/authentic2/authenticators.py              |  21 +-
 src/authentic2/backends/__init__.py           |  20 +-
 src/authentic2/backends/ldap_backend.py       |  51 +-
 src/authentic2/backends/models_backend.py     |   2 +-
 src/authentic2/cbv.py                         |  16 +
 src/authentic2/compat.py                      |  31 +-
 src/authentic2/compat_lasso.py                |  16 +
 src/authentic2/constants.py                   |  16 +
 src/authentic2/context_processors.py          |  20 +-
 src/authentic2/cors.py                        |  16 +
 src/authentic2/crypto.py                      |  23 +-
 src/authentic2/custom_user/__init__.py        |  16 +
 src/authentic2/custom_user/apps.py            |  16 +
 .../management/commands/changepassword.py     |  24 +-
 .../management/commands/fix-attributes.py     |  16 +
 src/authentic2/custom_user/managers.py        |  26 +-
 src/authentic2/custom_user/models.py          |  41 +-
 src/authentic2/data_transfer.py               |  16 +
 src/authentic2/decorators.py                  |  74 +-
 .../disco_service/disco_responder.py          |  80 +-
 src/authentic2/exponential_retry_timeout.py   |  16 +
 src/authentic2/forms/__init__.py              | 273 -------
 src/authentic2/forms/authentication.py        | 119 +++
 src/authentic2/forms/fields.py                |  16 +
 src/authentic2/forms/passwords.py             | 128 ++++
 src/authentic2/forms/profile.py               | 168 ++++
 .../forms.py => forms/registration.py}        | 122 +--
 src/authentic2/forms/utils.py                 |  33 +
 src/authentic2/forms/widgets.py               |  42 +-
 src/authentic2/hashers.py                     |  43 +-
 src/authentic2/hooks.py                       |  20 +-
 src/authentic2/http_utils.py                  |  17 +
 src/authentic2/idp/interactions.py            |  98 +--
 .../idp/management/commands/cleanup.py        |  11 -
 .../management/commands/cleanupauthentic.py   |  23 +-
 src/authentic2/idp/middleware.py              |   8 -
 src/authentic2/idp/models.py                  |   0
 src/authentic2/idp/saml/__init__.py           |  27 +-
 src/authentic2/idp/saml/app_settings.py       |  53 +-
 src/authentic2/idp/saml/backend.py            |  16 +
 src/authentic2/idp/saml/common.py             |  25 +-
 src/authentic2/idp/saml/saml2_endpoints.py    | 484 ++++++------
 src/authentic2/idp/saml/urls.py               |  16 +
 src/authentic2/idp/saml/views.py              |  21 +-
 src/authentic2/idp/signals.py                 |  21 +-
 src/authentic2/idp/templatetags/__init__.py   |   0
 .../idp/templatetags/breadcrumbs.py           | 134 ----
 src/authentic2/idp/urls.py                    |  20 +-
 src/authentic2/idp/utils.py                   |   0
 src/authentic2/idp/views.py                   |   0
 src/authentic2/ldap_utils.py                  |  17 +-
 src/authentic2/log_filters.py                 |  19 +-
 src/authentic2/logger.py                      |  19 +
 .../commands/clean-unused-accounts.py         |  78 +-
 .../management/commands/export_site.py        |  17 +-
 .../management/commands/import_site.py        |  16 +
 .../management/commands/load-ldif.py          |  30 +-
 .../management/commands/resetpassword.py      |  34 +-
 .../management/commands/slapd-shell.py        |  20 +-
 .../management/commands/sync-ldap-users.py    |  22 +-
 src/authentic2/manager/__init__.py            |  16 +
 src/authentic2/manager/app_settings.py        |  16 +
 src/authentic2/manager/apps.py                |  17 +-
 src/authentic2/manager/fields.py              |  16 +
 src/authentic2/manager/forms.py               |  67 +-
 src/authentic2/manager/models.py              |   0
 src/authentic2/manager/ou_views.py            |  16 +
 src/authentic2/manager/resources.py           |  23 +-
 src/authentic2/manager/role_views.py          |  53 +-
 src/authentic2/manager/service_views.py       |  17 +-
 src/authentic2/manager/tables.py              |  42 +-
 src/authentic2/manager/urls.py                |  23 +-
 src/authentic2/manager/user_views.py          |  39 +-
 src/authentic2/manager/utils.py               |  16 +-
 src/authentic2/manager/views.py               |  27 +-
 src/authentic2/manager/widgets.py             |  16 +
 src/authentic2/managers.py                    |  17 +-
 src/authentic2/middleware.py                  |  27 +-
 src/authentic2/models.py                      | 173 ++---
 src/authentic2/natural_key.py                 |  16 +
 src/authentic2/nonce/__init__.py              |  16 +
 src/authentic2/nonce/models.py                |  23 +-
 src/authentic2/nonce/utils.py                 |  41 +-
 src/authentic2/passwords.py                   |  18 +-
 src/authentic2/plugins.py                     |  39 +-
 src/authentic2/profile_forms.py               |  38 -
 src/authentic2/profile_urls.py                |  97 ---
 src/authentic2/profile_views.py               | 127 ----
 .../registration_backend/__init__.py          |   0
 src/authentic2/registration_backend/urls.py   |  23 -
 src/authentic2/registration_backend/views.py  | 416 ----------
 src/authentic2/saml/__init__.py               |  16 +
 src/authentic2/saml/admin.py                  | 138 ++--
 src/authentic2/saml/admin_views.py            |  25 +-
 src/authentic2/saml/app_settings.py           |  34 +-
 src/authentic2/saml/common.py                 |  59 +-
 src/authentic2/saml/fields.py                 |  45 +-
 src/authentic2/saml/forms.py                  |  18 +-
 src/authentic2/saml/lasso_helper.py           |  24 +-
 .../saml/management/commands/mapping.py       |  16 +
 .../saml/management/commands/sync-metadata.py |  55 +-
 src/authentic2/saml/managers.py               |  38 +-
 src/authentic2/saml/models.py                 | 381 +++++-----
 src/authentic2/saml/saml11utils.py            | 210 -----
 src/authentic2/saml/saml2utils.py             | 122 +--
 src/authentic2/saml/shibboleth/afp_parser.py  |  20 +
 src/authentic2/saml/shibboleth/utils.py       |  18 +-
 src/authentic2/saml/utils.py                  |  16 +
 src/authentic2/saml/x509utils.py              |  83 +-
 src/authentic2/serializers.py                 |  20 +-
 src/authentic2/settings.py                    |  76 +-
 .../registration_completion_choose.html       |   6 -
 .../registration_completion_form.html         |   6 -
 .../registration/registration_form.html       |   6 -
 src/authentic2/urls.py                        | 121 ++-
 src/authentic2/user_login_failure.py          |  25 +-
 src/authentic2/utils.py                       |  49 +-
 src/authentic2/validators.py                  |  18 +-
 src/authentic2/views.py                       | 715 +++++++++++++++---
 src/authentic2/widgets.py                     |  18 +-
 src/authentic2/wsgi.py                        |  21 +-
 src/authentic2_auth_oidc/__init__.py          |  17 +-
 src/authentic2_auth_oidc/admin.py             |  16 +
 src/authentic2_auth_oidc/app_settings.py      |  22 +-
 src/authentic2_auth_oidc/authenticators.py    |  16 +
 src/authentic2_auth_oidc/backends.py          |  16 +
 .../commands/oidc-register-issuer.py          |  19 +-
 src/authentic2_auth_oidc/managers.py          |  16 +
 src/authentic2_auth_oidc/models.py            |  16 +
 src/authentic2_auth_oidc/urls.py              |  16 +
 src/authentic2_auth_oidc/utils.py             |  28 +-
 src/authentic2_auth_oidc/views.py             |  16 +
 src/authentic2_auth_saml/__init__.py          |  17 +
 src/authentic2_auth_saml/adapters.py          |  16 +
 src/authentic2_auth_saml/app_settings.py      |  21 +-
 src/authentic2_auth_saml/authenticators.py    |  16 +
 src/authentic2_auth_saml/backends.py          |  16 +
 src/authentic2_auth_saml/models.py            |   0
 src/authentic2_auth_saml/urls.py              |  16 +
 src/authentic2_auth_saml/views.py             |   0
 src/authentic2_idp_cas/__init__.py            |  28 +-
 src/authentic2_idp_cas/admin.py               |  81 +-
 src/authentic2_idp_cas/app_settings.py        |  26 +-
 src/authentic2_idp_cas/constants.py           |  16 +
 src/authentic2_idp_cas/managers.py            |  19 +-
 src/authentic2_idp_cas/models.py              |  74 +-
 src/authentic2_idp_cas/urls.py                |  16 +
 src/authentic2_idp_cas/utils.py               |  23 +-
 src/authentic2_idp_cas/views.py               | 152 ++--
 src/authentic2_idp_oidc/__init__.py           |  17 +-
 src/authentic2_idp_oidc/admin.py              |  16 +
 src/authentic2_idp_oidc/app_settings.py       |  22 +-
 src/authentic2_idp_oidc/apps.py               |   6 +-
 src/authentic2_idp_oidc/managers.py           |  16 +
 src/authentic2_idp_oidc/models.py             |  20 +-
 src/authentic2_idp_oidc/urls.py               |  16 +
 src/authentic2_idp_oidc/utils.py              |  17 +-
 src/authentic2_idp_oidc/views.py              |  20 +-
 .../management/commands/provision.py          |  48 +-
 .../tests/test_ldap.py                        |  46 +-
 src/django_rbac/backends.py                   |   1 +
 src/django_rbac/context_processors.py         |   6 +-
 src/django_rbac/managers.py                   |   2 +-
 src/django_rbac/models.py                     |  21 +-
 src/django_rbac/test_settings.py              |  11 +-
 src/django_rbac/utils.py                      |  18 +-
 tests/cache_urls.py                           |  16 +
 tests/conftest.py                             |  16 +
 tests/settings.py                             |  16 +
 tests/test_a2_rbac.py                         |  16 +
 tests/test_admin.py                           |  16 +
 tests/test_all.py                             |  16 +
 tests/test_api.py                             |  16 +
 tests/test_attribute_kinds.py                 |  16 +
 tests/test_auth_oidc.py                       |  16 +
 tests/test_auth_saml.py                       |  16 +
 tests/test_backends.py                        |  16 +
 tests/test_cas.py                             |  26 +-
 tests/test_change_email.py                    |  16 +
 tests/test_cleanup.py                         |  16 +
 tests/test_commands.py                        |  16 +
 tests/test_concurrency.py                     |  16 +
 tests/test_crypto.py                          |  16 +
 tests/test_custom_user.py                     |  16 +
 tests/test_customfields.py                    |  16 +
 tests/test_data_transfer.py                   |  16 +
 tests/test_hashers.py                         |  16 +
 tests/test_idp_oidc.py                        |  16 +
 tests/test_idp_saml2.py                       |  16 +
 tests/test_import_export_site_cmd.py          |  16 +
 tests/test_ldap.py                            |   4 +-
 tests/test_login.py                           |   2 +-
 tests/test_manager.py                         |  15 +
 tests/test_natural_key.py                     |  15 +
 tests/test_ou_manager.py                      |  15 +
 tests/test_password_reset.py                  |  15 +
 tests/test_profile.py                         |  15 +
 tests/test_registration.py                    |  15 +
 tests/test_role_manager.py                    |  15 +
 tests/test_user_manager.py                    |  15 +
 tests/test_user_model.py                      |   5 +-
 tests/test_utils.py                           |  25 +-
 tests/test_views.py                           |  17 +
 tests/test_widget_datetimepicker.py           |  17 +
 tests/utils.py                                |  17 +
 244 files changed, 6197 insertions(+), 3306 deletions(-)
 delete mode 100644 src/authentic2/attribute_aggregator/models.py
 delete mode 100644 src/authentic2/auth2_auth/models.py
 create mode 100644 src/authentic2/forms/authentication.py
 create mode 100644 src/authentic2/forms/passwords.py
 create mode 100644 src/authentic2/forms/profile.py
 rename src/authentic2/{registration_backend/forms.py => forms/registration.py} (53%)
 create mode 100644 src/authentic2/forms/utils.py
 delete mode 100644 src/authentic2/idp/management/commands/cleanup.py
 delete mode 100644 src/authentic2/idp/middleware.py
 delete mode 100644 src/authentic2/idp/models.py
 delete mode 100644 src/authentic2/idp/templatetags/__init__.py
 delete mode 100644 src/authentic2/idp/templatetags/breadcrumbs.py
 delete mode 100644 src/authentic2/idp/utils.py
 delete mode 100644 src/authentic2/idp/views.py
 delete mode 100644 src/authentic2/manager/models.py
 delete mode 100644 src/authentic2/profile_forms.py
 delete mode 100644 src/authentic2/profile_urls.py
 delete mode 100644 src/authentic2/profile_views.py
 delete mode 100644 src/authentic2/registration_backend/__init__.py
 delete mode 100644 src/authentic2/registration_backend/urls.py
 delete mode 100644 src/authentic2/registration_backend/views.py
 delete mode 100644 src/authentic2/saml/saml11utils.py
 delete mode 100644 src/authentic2_auth_saml/models.py
 delete mode 100644 src/authentic2_auth_saml/views.py
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',
75
                    'proxy',
76
                ]
77
             }),
78
            (_('Logout'), {
79
                'fields': [
80
                    'logout_url',
81
                    'logout_use_iframe',
82
                    'logout_use_iframe_timeout',
83
                ],
84
             }))
88
        (None, {
89
            'fields': [
90
                'name',
91
                'slug',
92
                'ou',
93
                'urls',
94
                'identifier_attribute',
95
                'proxy',
96
            ]}),
97
        (_('Logout'), {
98
            'fields': [
99
                'logout_url',
100
                'logout_use_iframe',
101
                'logout_use_iframe_timeout',
102
            ]}))
85 103
    inlines = [AttributeInlineAdmin]
86 104

  
105

  
87 106
class TicketAdmin(CleanupAdminMixin, admin.ModelAdmin):
88 107
    list_display = (
89
            'ticket_id',
90
            'validity',
91
            'renew',
92
            'service',
93
            'service_url',
94
            'user',
95
            'creation',
96
            'expire'
108
        'ticket_id',
109
        'validity',
110
        'renew',
111
        'service',
112
        'service_url',
113
        'user',
114
        'creation',
115
        'expire'
97 116
    )
98 117

  
99 118
admin.site.register(models.Service, ServiceAdmin)
src/authentic2_idp_cas/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 = {
3
            'ENABLE': False,
4
            # allow do tisable check of pgt url for testing purpose
5
            'CHECK_PGT_URL': True,
22
        'ENABLE': False,
23
        # allow do tisable check of pgt url for testing purpose
24
        'CHECK_PGT_URL': True,
6 25
    }
7 26

  
8 27
    def __init__(self, prefix):
......
20 39

  
21 40
# Ugly? Guido recommends this himself ...
22 41
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
23
import sys
24 42
app_settings = AppSettings('A2_IDP_CAS_')
25 43
app_settings.__name__ = __name__
26 44
sys.modules[__name__] = app_settings
src/authentic2_idp_cas/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
# Constants #
2 18
CAS_NAMESPACE    = 'http://www.yale.edu/tp/cas'
3 19
RENEW_PARAM      = 'renew'
src/authentic2_idp_cas/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

  
3 19
from django.db import models
......
14 30
    def cleanup(self):
15 31
        '''Delete old tickets'''
16 32
        qs = self.filter(expire__lt=now())
17
        qs |= self.filter(expire__isnull=True,
18
                creation__lt=now()-timedelta(seconds=300))
33
        qs |= self.filter(expire__isnull=True, creation__lt=now() - timedelta(seconds=300))
19 34
        qs.delete()
20 35

  
21 36

  
src/authentic2_idp_cas/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 six
3 19
from django.utils.translation import ugettext_lazy as _
......
6 22
from django.core.exceptions import ValidationError
7 23

  
8 24
from authentic2.models import LogoutUrlAbstract, Service
9
from authentic2 import compat
10 25
from authentic2.utils import check_session_key
11 26

  
12 27
from . import managers, utils, constants
13 28

  
14 29
url_validator = URLValidator(schemes=['http', 'https', 'ftp', 'ftps', 'imap', 'imaps', 'sieve', 'smtp', 'smtps', 'ssh'])
15 30

  
31

  
16 32
@six.python_2_unicode_compatible
17 33
class Service(LogoutUrlAbstract, Service):
18
    urls = models.TextField(max_length=128,
19
            verbose_name=_('urls'))
20
    identifier_attribute = models.CharField(max_length=64,
21
            verbose_name=_('attribute name'), blank=False)
22
    proxy = models.ManyToManyField('self',
23
            blank=True,
24
            verbose_name=_('proxy'),
25
            help_text=_('services who can request proxy tickets for this service'))
34
    urls = models.TextField(max_length=128, verbose_name=_('urls'))
35
    identifier_attribute = models.CharField(max_length=64, verbose_name=_('attribute name'), blank=False)
36
    proxy = models.ManyToManyField(
37
        'self',
38
        blank=True,
39
        verbose_name=_('proxy'),
40
        help_text=_('services who can request proxy tickets for this service'))
26 41

  
27 42
    objects = managers.ServiceManager()
28 43

  
......
66 81
@six.python_2_unicode_compatible
67 82
class Attribute(models.Model):
68 83
    service = models.ForeignKey(Service, verbose_name=_('service'))
69
    slug    = models.SlugField(verbose_name=_('slug'))
70
    attribute_name = models.CharField(max_length=64,
71
        verbose_name=_('attribute name'), blank=False)
72
    enabled = models.BooleanField(
73
            verbose_name=_('enabled'),
74
            default=True)
84
    slug = models.SlugField(verbose_name=_('slug'))
85
    attribute_name = models.CharField(max_length=64, verbose_name=_('attribute name'), blank=False)
86
    enabled = models.BooleanField(verbose_name=_('enabled'), default=True)
75 87

  
76 88
    def __str__(self):
77 89
        return u'%s <- %s' % (self.slug, self.attribute_name)
......
81 93
        verbose_name_plural = _('CAS attributes')
82 94
        unique_together = (('service', 'slug', 'attribute_name',),)
83 95

  
96

  
84 97
def make_uuid():
85 98
    return utils.make_id(constants.SERVICE_TICKET_PREFIX)
86 99

  
100

  
87 101
@six.python_2_unicode_compatible
88 102
class Ticket(models.Model):
89 103
    '''Session ticket with a CAS 1.0 or 2.0 consumer'''
90

  
91
    ticket_id   = models.CharField(max_length=64,
92
                    verbose_name=_('ticket id'),
93
                    unique=True,
94
                    default=make_uuid)
95
    renew       = models.BooleanField(default=False,
96
            verbose_name=_('fresh authentication'))
97
    validity    = models.BooleanField(default=False,
98
            verbose_name=_('valid'))
99
    service     = models.ForeignKey(Service, verbose_name=_('service'))
104
    ticket_id = models.CharField(max_length=64, verbose_name=_('ticket id'), unique=True, default=make_uuid)
105
    renew = models.BooleanField(default=False, verbose_name=_('fresh authentication'))
106
    validity = models.BooleanField(default=False, verbose_name=_('valid'))
107
    service = models.ForeignKey(Service, verbose_name=_('service'))
100 108
    service_url = models.TextField(verbose_name=_('service URL'), blank=True, default='')
101
    user        = models.ForeignKey(compat.user_model_label, max_length=128,
102
            blank=True, null=True, verbose_name=_('user'))
103
    creation    = models.DateTimeField(auto_now_add=True,
104
            verbose_name=_('creation'))
105
    expire      = models.DateTimeField(
106
            verbose_name=_('expire'), blank=True, null=True)
107
    session_key = models.CharField(max_length=64, db_index=True, blank=True,
108
            verbose_name=_('django session key'), default='')
109
    proxies = models.TextField(
110
            verbose_name=_('proxies'), blank=True, default='')
109
    user = models.ForeignKey('custom_user.User', max_length=128, blank=True, null=True, verbose_name=_('user'))
110
    creation = models.DateTimeField(auto_now_add=True, verbose_name=_('creation'))
111
    expire = models.DateTimeField(verbose_name=_('expire'), blank=True, null=True)
112
    session_key = models.CharField(
113
        max_length=64, db_index=True, blank=True, verbose_name=_('django session key'), default='')
114
    proxies = models.TextField(verbose_name=_('proxies'), blank=True, default='')
111 115

  
112 116
    objects = managers.TicketManager()
113 117

  
src/authentic2_idp_cas/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_cas/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 random
2 18
import string
3 19

  
4
ALPHABET = string.ascii_letters+string.digits+'-'
20
ALPHABET = string.ascii_letters + string.digits + '-'
21

  
5 22

  
6 23
def make_id(prefix='', length=29):
7 24
    '''Generate CAS tickets identifiers'''
8
    l = length-len(prefix)
9
    content = ( random.SystemRandom().choice(ALPHABET) for x in range(l) )
25
    c = length - len(prefix)
26
    content = (random.SystemRandom().choice(ALPHABET) for x in range(c) )
10 27
    return prefix + ''.join(content)
src/authentic2_idp_cas/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
from datetime import timedelta
3 19
from xml.etree import ElementTree as ET
......
11 27
from django.utils.timezone import now
12 28

  
13 29
from authentic2.utils import (get_user_from_session_key, make_url,
14
        login_require, find_authentication_event, redirect, normalize_attribute_values,
15
        attribute_values_to_identifier)
30
                              login_require, find_authentication_event,
31
                              redirect, normalize_attribute_values,
32
                              attribute_values_to_identifier)
16 33
from authentic2.attributes_ng.engine import get_attributes
17 34
from authentic2.constants import NONCE_FIELD_NAME
18 35
from authentic2.views import logout as logout_view
......
20 37

  
21 38
from authentic2_idp_cas.models import Ticket, Service
22 39
from authentic2_idp_cas.utils import make_id
23
from authentic2_idp_cas.constants import (SERVICE_PARAM, RENEW_PARAM, GATEWAY_PARAM,
24
        TICKET_PARAM, CANCEL_PARAM, SERVICE_TICKET_PREFIX,
25
        INVALID_REQUEST_ERROR, INVALID_TICKET_SPEC_ERROR,
26
        INVALID_SERVICE_ERROR, INVALID_TICKET_ERROR,
27
        CAS10_VALIDATION_FAILURE, CAS20_VALIDATION_FAILURE,
28
        SERVICE_RESPONSE_ELT, AUTHENTICATION_SUCCESS_ELT, USER_ELT,
29
        PGT_URL_PARAM, PGT_IOU_PARAM, SESSION_CAS_LOGOUTS,
30
        CAS10_VALIDATION_SUCCESS, PGT_ELT, PROXIES_ELT, PROXY_ELT,
31
        PGT_PREFIX, PGT_IOU_PREFIX, PT_PREFIX, TARGET_SERVICE_PARAM,
32
        BAD_PGT_ERROR, INVALID_TARGET_SERVICE_ERROR, PROXY_UNAUTHORIZED_ERROR,
33
        PGT_PARAM, PGT_ID_PARAM, CAS20_PROXY_FAILURE, PROXY_SUCCESS_ELT,
34
        PROXY_TICKET_ELT, INTERNAL_ERROR, CAS_NAMESPACE, ATTRIBUTES_ELT)
40
from authentic2_idp_cas.constants import (
41
    SERVICE_PARAM, RENEW_PARAM, GATEWAY_PARAM, TICKET_PARAM, CANCEL_PARAM,
42
    SERVICE_TICKET_PREFIX, INVALID_REQUEST_ERROR, INVALID_TICKET_SPEC_ERROR,
43
    INVALID_SERVICE_ERROR, INVALID_TICKET_ERROR, CAS10_VALIDATION_FAILURE,
44
    CAS20_VALIDATION_FAILURE, SERVICE_RESPONSE_ELT, AUTHENTICATION_SUCCESS_ELT,
45
    USER_ELT, PGT_URL_PARAM, PGT_IOU_PARAM, SESSION_CAS_LOGOUTS,
46
    CAS10_VALIDATION_SUCCESS, PGT_ELT, PROXIES_ELT, PROXY_ELT, PGT_PREFIX,
47
    PGT_IOU_PREFIX, PT_PREFIX, TARGET_SERVICE_PARAM, BAD_PGT_ERROR,
48
    INVALID_TARGET_SERVICE_ERROR, PROXY_UNAUTHORIZED_ERROR, PGT_PARAM,
49
    PGT_ID_PARAM, CAS20_PROXY_FAILURE, PROXY_SUCCESS_ELT, PROXY_TICKET_ELT,
50
    INTERNAL_ERROR, CAS_NAMESPACE, ATTRIBUTES_ELT)
35 51
from . import app_settings
36 52

  
37 53
try:
......
55 71

  
56 72
    def redirect_to_service(self, request, st):
57 73
        if not st.valid():
58
            return self.failure(request, st.service_url, 'service ticket id is '
59
                    'not valid')
74
            return self.failure(request, st.service_url, 'service ticket id is not valid')
60 75
        else:
61 76
            return self.return_ticket(request, st)
62 77

  
......
70 85
        st.save()
71 86
        if st.service.logout_url:
72 87
            request.session.setdefault(SESSION_CAS_LOGOUTS, []).append((
73
                    st.service.name, 
74
                    st.service.get_logout_url(request),
75
                    st.service.logout_use_iframe,
76
                    st.service.logout_use_iframe_timeout))
88
                st.service.name,
89
                st.service.get_logout_url(request),
90
                st.service.logout_use_iframe,
91
                st.service.logout_use_iframe_timeout))
77 92

  
78 93
    def authenticate(self, request, st):
79 94
        '''
......
84 99
        nonce = st.ticket_id
85 100
        next_url = make_url('a2-idp-cas-continue', params={
86 101
            SERVICE_PARAM: st.service_url, NONCE_FIELD_NAME: nonce})
87
        return login_require(request, next_url=next_url,
88
                params={NONCE_FIELD_NAME: nonce})
102
        return login_require(request, next_url=next_url, params={NONCE_FIELD_NAME: nonce})
89 103

  
90 104

  
91 105
class LoginView(CasMixin, View):
......
102 116
        if not model:
103 117
            return self.failure(request, service, 'service unknown')
104 118
        if renew and gateway:
105
            return self.failure(request, service, 'renew and gateway cannot be requested '
106
                    'at the same time')
119
            return self.failure(request, service, 'renew and gateway cannot be requested at the same time')
107 120

  
108 121
        hooks.call_hooks('event', name='sso-request', service=model)
109 122

  
......
113 126
        service = service[:4096]
114 127
        st.service_url = service
115 128
        st.renew = renew
116
        self.logger.debug('login request from %r renew: %s gateway: %s',
117
                service, renew, gateway)
129
        self.logger.debug('login request from %r renew: %s gateway: %s', service, renew, gateway)
118 130
        if self.must_authenticate(request, renew, gateway):
119 131
            st.save()
120 132
            return self.authenticate(request, st)
......
175 187
        if st.valid():
176 188
            hooks.call_hooks('event', name='sso-success', service=st.service, user=st.user)
177 189
            return redirect(request, service, params={'ticket': st.ticket_id})
178
        # Should not happen 
190
        # Should not happen
179 191
        assert False
180 192

  
181 193

  
......
214 226
            attributes = self.get_attributes(request, st)
215 227
            if st.service.identifier_attribute not in attributes:
216 228
                self.logger.error('unable to compute an identifier for user %r and service %s',
217
                        six.text_type(st.user), st.service_url)
229
                                  six.text_type(st.user), st.service_url)
218 230
                return self.validation_failure(request, service, INTERNAL_ERROR)
219 231
            # Compute user identifier
220
            identifier = attribute_values_to_identifier(
221
                    attributes[st.service.identifier_attribute])
232
            identifier = attribute_values_to_identifier(attributes[st.service.identifier_attribute])
222 233
            return self.validation_success(request, st, identifier)
223
        except:
224
            raise
234
        except Exception:
225 235
            self.logger.exception('internal server error')
226 236
            return self.validation_failure(request, service, INTERNAL_ERROR)
227 237

  
......
230 240
        if not hasattr(st, 'attributes'):
231 241
            wanted_attributes = st.service.get_wanted_attributes()
232 242
            user = get_user_from_session_key(st.session_key)
233
            assert user.pk # not an annymous user
234
            assert st.user_id == user.pk # session user matches ticket user
243
            assert user.pk  # not an annymous user
244
            assert st.user_id == user.pk  # session user matches ticket user
235 245
            st.attributes = get_attributes({
236 246
                'request': request,
237 247
                'user': user,
......
245 255
        return self.real_validation_failure(request, service, code)
246 256

  
247 257
    def validation_success(self, request, st, identifier):
248
        self.logger.info('validation success service: %r ticket: %s '
249
                'user: %r identifier: %r', st.service_url, st.ticket_id, six.text_type(st.user), identifier)
258
        self.logger.info('validation success service: %r ticket: %s user: %r identifier: %r',
259
                         st.service_url, st.ticket_id, six.text_type(st.user), identifier)
250 260
        return self.real_validation_success(request, st, identifier)
251 261

  
252 262

  
253 263
class ValidateView(ValidateBaseView):
254 264
    def real_validation_failure(self, request, service, code):
255
        return HttpResponse(CAS10_VALIDATION_FAILURE,
256
                content_type='text/plain')
265
        return HttpResponse(CAS10_VALIDATION_FAILURE, content_type='text/plain')
257 266

  
258 267
    def real_validation_success(self, request, st, identifier):
259
        return HttpResponse(CAS10_VALIDATION_SUCCESS % identifier,
260
                content_type='text/plain')
268
        return HttpResponse(CAS10_VALIDATION_SUCCESS % identifier, content_type='text/plain')
261 269

  
262 270

  
263 271
class ServiceValidateView(ValidateBaseView):
......
265 273

  
266 274
    def real_validation_failure(self, request, service, code, message=''):
267 275
        message = message or self.get_cas20_error_message(code)
268
        return HttpResponse(CAS20_VALIDATION_FAILURE % (code, message),
269
                content_type='text/xml')
276
        return HttpResponse(CAS20_VALIDATION_FAILURE % (code, message), content_type='text/xml')
270 277

  
271 278
    def get_cas20_error_message(self, code):
272
        return '' # FIXME
279
        return ''  # FIXME
273 280

  
274 281
    def real_validation_success(self, request, st, identifier):
275 282
        root = ET.Element(SERVICE_RESPONSE_ELT)
......
278 285
        user.text = six.text_type(identifier)
279 286
        self.provision_pgt(request, st, success)
280 287
        self.provision_attributes(request, st, success)
281
        return HttpResponse(ET.tostring(root, encoding='utf-8'),
282
                content_type='text/xml')
288
        return HttpResponse(ET.tostring(root, encoding='utf-8'), content_type='text/xml')
283 289

  
284 290
    def provision_attributes(self, request, st, success):
285 291
        '''Add attributes to the CAS 2.0 ticket'''
......
300 306
                attribute_elt = ET.SubElement(attributes_elt, '{%s}%s' % (CAS_NAMESPACE, key))
301 307
                attribute_elt.text = six.text_type(value)
302 308

  
303

  
304 309
    def provision_pgt(self, request, st, success):
305 310
        '''Provision a PGT ticket if requested
306 311
        '''
......
312 317
            return
313 318
        # PGT URL must be declared
314 319
        if not st.service.match_service(pgt_url):
315
            self.logger.warning('pgtUrl %r does not match service %r',
316
                pgt_url, st.service.slug)
320
            self.logger.warning('pgtUrl %r does not match service %r', pgt_url, st.service.slug)
317 321
        pgt = make_id(PGT_PREFIX)
318 322
        pgt_iou = make_id(PGT_IOU_PREFIX)
319 323
        # Skip PGT_URL check for testing purpose
320 324
        # instead store PGT_IOU / PGT association in session
321 325
        if app_settings.CHECK_PGT_URL:
322
            response = requests.get(pgt_url, params={
323
               PGT_ID_PARAM: pgt,
324
               PGT_IOU_PARAM: pgt_iou})
326
            response = requests.get(pgt_url,
327
                                    params={
328
                                        PGT_ID_PARAM: pgt,
329
                                        PGT_IOU_PARAM: pgt_iou})
325 330
            if response.status_code != 200:
326
                self.logger.warning('pgtUrl %r returned non 200 code: %d',
327
                    pgt_url, response.status_code)
331
                self.logger.warning('pgtUrl %r returned non 200 code: %d', pgt_url, response.status_code)
328 332
                return
329 333
        else:
330 334
            request.session[pgt_iou] = pgt
331 335
        proxies = ('%s %s' % (pgt_url, st.proxies)).strip()
332 336
        # Save the PGT ticket
333 337
        Ticket.objects.create(
334
                ticket_id=pgt,
335
                expire=None,
336
                service=st.service,
337
                service_url=st.service_url,
338
                validity=True,
339
                user=st.user,
340
                session_key=st.session_key,
341
                proxies=proxies)
338
            ticket_id=pgt,
339
            expire=None,
340
            service=st.service,
341
            service_url=st.service_url,
342
            validity=True,
343
            user=st.user,
344
            session_key=st.session_key,
345
            proxies=proxies)
342 346
        user = ET.SubElement(success, PGT_ELT)
343 347
        user.text = pgt_iou
344 348
        if self.add_proxies:
......
350 354

  
351 355
class ProxyView(View):
352 356
    http_method_names = ['get']
353
    
357

  
354 358
    def get(self, request):
355 359
        pgt = request.GET.get(PGT_PARAM)
356 360
        target_service_url = request.GET.get(TARGET_SERVICE_PARAM)
357 361
        if not pgt or not target_service_url:
358 362
            return self.validation_failure(INVALID_REQUEST_ERROR,
359
                    "'pgt' and 'targetService' parameters are both required")
363
                                           "'pgt' and 'targetService' parameters are both required")
360 364
        if not pgt.startswith(PGT_PREFIX):
361 365
            return self.validation_failure(BAD_PGT_ERROR,
362
                    'a proxy granting ticket must start with PGT-')
366
                                           'a proxy granting ticket must start with PGT-')
363 367
        try:
364 368
            pgt = Ticket.objects.get(ticket_id=pgt)
365 369
        except Ticket.DoesNotExist:
366 370
            pgt = None
367 371
        if pgt is None:
368
            return self.validation_failure(BAD_PGT_ERROR, 'pgt does not '
369
                    'exist')
372
            return self.validation_failure(BAD_PGT_ERROR, 'pgt does not exist')
370 373
        if not pgt.valid():
371 374
            pgt.delete()
372 375
            return self.validation_failure(BAD_PGT_ERROR, 'session has expired')
......
374 377
        # No target service exists for this url, maybe the URL is missing from
375 378
        # the urls field
376 379
        if not target_service:
377
            return self.validation_failure(INVALID_TARGET_SERVICE_ERROR,
378
                    'target service is invalid')
380
            return self.validation_failure(INVALID_TARGET_SERVICE_ERROR, 'target service is invalid')
379 381
        # Verify that the requested service is authorized to get proxy tickets
380 382
        # for the target service
381 383
        if not target_service.proxy.filter(pk=pgt.service_id).exists():
382
            return self.validation_failure(PROXY_UNAUTHORIZED_ERROR,
383
                    'proxying to the target service is forbidden')
384
            return self.validation_failure(PROXY_UNAUTHORIZED_ERROR, 'proxying to the target service is forbidden')
384 385
        pt = Ticket.objects.create(
385 386
            ticket_id=make_id(PT_PREFIX),
386 387
            validity=True,
387
            expire=now()+timedelta(seconds=60),
388
            expire=now() + timedelta(seconds=60),
388 389
            service=target_service,
389 390
            service_url=target_service_url,
390 391
            user=pgt.user,
......
393 394
        return self.validation_success(request, pt)
394 395

  
395 396
    def validation_failure(self, code, reason):
396
        return HttpResponse(CAS20_PROXY_FAILURE % (code, reason),
397
                content_type='text/xml')
397
        return HttpResponse(CAS20_PROXY_FAILURE % (code, reason), content_type='text/xml')
398 398

  
399 399
    def validation_success(self, request, pt):
400 400
        root = ET.Element(SERVICE_RESPONSE_ELT)
401 401
        success = ET.SubElement(root, PROXY_SUCCESS_ELT)
402 402
        proxy_ticket = ET.SubElement(success, PROXY_TICKET_ELT)
403 403
        proxy_ticket.text = pt.ticket_id
404
        return HttpResponse(ET.tostring(root, encoding='utf-8'),
405
                content_type='text/xml')
404
        return HttpResponse(ET.tostring(root, encoding='utf-8'), content_type='text/xml')
406 405

  
407 406

  
408 407
class ProxyValidateView(ServiceValidateView):
......
420 419
        if referrer:
421 420
            model = Service.objects.for_service(referrer)
422 421
            if model:
423
                return logout_view(request, next_url=next_url,
424
                        check_referer=False, do_local=False)
422
                return logout_view(request, next_url=next_url, check_referer=False, do_local=False)
425 423
        return redirect(request, next_url)
426 424

  
427 425
login = LoginView.as_view()
src/authentic2_idp_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
from django.template.loader import render_to_string
2
from django.utils.translation import ugettext_lazy as _
3 18

  
4 19
default_app_config = 'authentic2_idp_oidc.apps.AppConfig'
5 20

  
src/authentic2_idp_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 import forms
2 18
from django.contrib import admin
3 19
from django.utils.functional import curry
src/authentic2_idp_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()
......
34 53
    def IDTOKEN_DURATION(self):
35 54
        return self._setting('IDTOKEN_DURATION', 30)
36 55

  
37

  
38
import sys
39

  
40 56
app_settings = AppSettings('A2_IDP_OIDC_')
41 57
app_settings.__name__ = __name__
42 58
sys.modules[__name__] = app_settings
src/authentic2_idp_oidc/apps.py
1
# authentic2_idp_oidc - Authentic2 OIDC IdP plugin
2
# Copyright (C) 2017 Entr'ouvert
1
# authentic2 - versatile identity manager
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
......
17 17
import django.apps
18 18
from django.utils.encoding import smart_bytes
19 19

  
20
from rest_framework.exceptions import APIException
21

  
22 20

  
23 21
class AppConfig(django.apps.AppConfig):
24 22
        name = 'authentic2_idp_oidc'
src/authentic2_idp_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 import Manager
2 18
from django.utils.timezone import now
3 19

  
src/authentic2_idp_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
from importlib import import_module
3 19

  
4 20
from django.db import models
5
from django.contrib.contenttypes.models import ContentType
6 21
from django.core.validators import URLValidator
7 22
from django.core.exceptions import ValidationError, ImproperlyConfigured
8 23
from django.utils.translation import ugettext_lazy as _
......
11 26
from django.utils.timezone import now
12 27
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
13 28

  
14
from authentic2.managers import GenericManager
29
from authentic2.a2_rbac.models import OrganizationalUnit
15 30
from authentic2.models import Service
16 31
from authentic2.utils import to_iter
17 32

  
......
305 320
            self.scopes)
306 321

  
307 322
# Add generic field to a2_rbac.OrganizationalUnit
308
from authentic2.a2_rbac.models import OrganizationalUnit
309 323
GenericRelation('authentic2_idp_oidc.OIDCAuthorization',
310 324
                content_type_field='client_ct',
311 325
                object_id_field='client_id').contribute_to_class(
src/authentic2_idp_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_idp_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 json
2 18
import hashlib
3 19
import base64
......
10 26
from django.conf import settings
11 27
from django.utils import six
12 28
from django.utils.encoding import force_bytes, force_text
13
from django.utils import six
... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.