Projet

Général

Profil

0001-WIP-add-journal-application-20695.patch

Paul Marillonnet, 27 novembre 2018 15:07

Télécharger (36,8 ko)

Voir les différences:

Subject: [PATCH] WIP add journal application (#20695)

 MANIFEST.in                                   |   1 +
 setup.py                                      |   1 +
 src/authentic2/app_settings.py                |   1 +
 .../authentic2/manager/user_journal.html      |  10 +
 src/authentic2/manager/urls.py                |   3 +
 src/authentic2/manager/user_views.py          |  13 ++
 src/authentic2/profile_urls.py                |   4 +-
 src/authentic2/settings.py                    |   1 +
 .../authentic2/user_profile_journal.html      |  14 ++
 src/authentic2/views.py                       |  23 +-
 src/authentic2_journal/__init__.py            |  21 ++
 src/authentic2_journal/admin.py               |   0
 src/authentic2_journal/app_settings.py        |  38 +++
 src/authentic2_journal/apps.py                |  44 ++++
 src/authentic2_journal/event_logger.py        |  61 +++++
 src/authentic2_journal/migrations/__init__.py |   0
 src/authentic2_journal/models.py              |  84 +++++++
 src/authentic2_journal/tables.py              |  32 +++
 .../templatetags/__init__.py                  |   0
 .../templatetags/authentic2_journal.py        |  26 +++
 src/authentic2_journal/utils.py               |  88 +++++++
 src/authentic2_journal/views.py               |  35 +++
 tests/conftest.py                             |   5 +
 tests/test_journal.py                         | 216 ++++++++++++++++++
 24 files changed, 719 insertions(+), 2 deletions(-)
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/user_journal.html
 create mode 100644 src/authentic2/templates/authentic2/user_profile_journal.html
 create mode 100644 src/authentic2_journal/__init__.py
 create mode 100644 src/authentic2_journal/admin.py
 create mode 100644 src/authentic2_journal/app_settings.py
 create mode 100644 src/authentic2_journal/apps.py
 create mode 100644 src/authentic2_journal/event_logger.py
 create mode 100644 src/authentic2_journal/migrations/__init__.py
 create mode 100644 src/authentic2_journal/models.py
 create mode 100644 src/authentic2_journal/tables.py
 create mode 100644 src/authentic2_journal/templatetags/__init__.py
 create mode 100644 src/authentic2_journal/templatetags/authentic2_journal.py
 create mode 100644 src/authentic2_journal/utils.py
 create mode 100644 src/authentic2_journal/views.py
 create mode 100644 tests/test_journal.py
MANIFEST.in
41 41
recursive-include src/authentic2_auth_saml/locale *.po *.mo
42 42
recursive-include src/authentic2_auth_oidc/locale *.po *.mo
43 43
recursive-include src/authentic2_idp_oidc/locale *.po *.mo
44
recursive-include src/authentic2_journal/locale *.po *.mo
44 45

  
45 46
recursive-include src/authentic2 README  xrds.xml *.txt yadis.xrdf
46 47
recursive-include src/authentic2_provisionning_ldap/tests *.ldif
setup.py
165 165
              'authentic2-idp-cas = authentic2_idp_cas:Plugin',
166 166
              'authentic2-idp-oidc = authentic2_idp_oidc:Plugin',
167 167
              'authentic2-provisionning-ldap = authentic2_provisionning_ldap:Plugin',
168
              'authentic2-journal = authentic2_journal:Plugin',
168 169
          ],
169 170
      })
src/authentic2/app_settings.py
207 207
    A2_ACCOUNTS_URL=Setting(default=None, definition='IdP has no account page, redirect to this one.'),
208 208
    A2_CACHE_ENABLED=Setting(default=True, definition='Disable all cache decorators for testing purpose.'),
209 209
    A2_ACCEPT_EMAIL_AUTHENTICATION=Setting(default=True, definition='Enable authentication by email'),
210
    A2_JOURNAL_EVENT_LOGGER=Setting(default=None, definition='Event logger class for the journal app.'),
210 211

  
211 212
)
212 213

  
src/authentic2/manager/templates/authentic2/manager/user_journal.html
1
{% extends "authentic2/manager/base.html" %}
2
{% load i18n staticfiles django_tables2 %}
3

  
4
{% block page_title %}
5
{% trans "User events journal" %}
6
{% endblock %}
7

  
8
{% block content %}
9
  {% render_table table "authentic2/manager/table.html" %}
10
{% endblock %}
src/authentic2/manager/urls.py
52 52
        url(r'^users/uuid:(?P<slug>[a-z0-9]+)/change-email/$',
53 53
            user_views.user_change_email,
54 54
            name='a2-manager-user-by-uuid-change-email'),
55
        url('^users/(?P<pk>\d+)/journal/$',
56
            user_views.user_journal,
57
            name='a2-manager-user-journal'),
55 58

  
56 59
        # Authentic2 roles
57 60
        url(r'^roles/$', role_views.listing,
src/authentic2/manager/user_views.py
22 22
from authentic2.utils import switch_user, send_password_reset_mail, redirect, send_email_change_email
23 23
from authentic2.a2_rbac.utils import get_default_ou
24 24
from authentic2 import hooks
25
from authentic2_journal.views import ManagerBaseJournal
26
from authentic2_journal.tables import UserEventsTable
25 27
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
26 28

  
27 29

  
......
510 512

  
511 513

  
512 514
user_delete = UserDeleteView.as_view()
515

  
516

  
517
class UserJournal(ManagerBaseJournal):
518
    model = get_user_model()
519
    table_class = UserEventsTable
520
    template_name = 'authentic2/manager/user_journal.html'
521
    permissions = ['custom_user.view_user']
522
    filter_table_by_perm = False
523

  
524

  
525
user_journal = UserJournal.as_view()
src/authentic2/profile_urls.py
9 9

  
10 10
from authentic2.utils import import_module_or_class, redirect
11 11
from . import app_settings, decorators, profile_views, hooks
12
from .views import (logged_in, edit_profile, email_change, email_change_verify, profile)
12
from .views import (logged_in, edit_profile, email_change, email_change_verify, profile,
13
        user_profile_journal)
13 14

  
14 15
SET_PASSWORD_FORM_CLASS = import_module_or_class(
15 16
        app_settings.A2_REGISTRATION_SET_PASSWORD_FORM_CLASS)
......
94 95
        auth_views.password_reset_done,
95 96
        name='auth_password_reset_done'),
96 97
    url(r'^switch-back/$', profile_views.switch_back, name='a2-switch-back'),
98
    url('^journal/$', user_profile_journal, name='user-profile-journal'),
97 99
]
src/authentic2/settings.py
126 126
    'authentic2.disco_service',
127 127
    'authentic2.manager',
128 128
    'authentic2_provisionning_ldap',
129
    'authentic2_journal',
129 130
    'authentic2',
130 131
    'django_rbac',
131 132
    'authentic2.a2_rbac',
src/authentic2/templates/authentic2/user_profile_journal.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3
{% load authentic2_journal %}
4

  
5
{% block content %}
6
  <h1>{% trans "User journal" %}</h1>
7
  <ul>
8
  {% for reference in object_list %}
9
      <li>{{ reference.timestamp }} - {{ reference.content|to_representation }}</li>
10
  {% empty %}
11
      <li>{% trans "No journal entry yet." %}</li>
12
  {% endfor %}
13
  </ul>
14
{% endblock %}
src/authentic2/views.py
13 13
from django.views.generic.edit import UpdateView, FormView
14 14
from django.views.generic import RedirectView, TemplateView
15 15
from django.views.generic.base import View
16
from django.views.generic.list import ListView
16 17
from django.contrib.auth import SESSION_KEY
17 18
from django import http, shortcuts
18 19
from django.core import mail, signing
......
21 22
from django.contrib import messages
22 23
from django.utils.translation import ugettext as _
23 24
from django.contrib.auth import logout as auth_logout
24
from django.contrib.auth import REDIRECT_FIELD_NAME
25
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
26
from django.contrib.contenttypes.models import ContentType
25 27
from django.http import (HttpResponseRedirect, HttpResponseForbidden,
26 28
    HttpResponse)
27 29
from django.core.exceptions import PermissionDenied
......
37 39

  
38 40

  
39 41
from . import (utils, app_settings, forms, compat, decorators, constants, models, cbv, hooks)
42
from authentic2_journal.models import Reference
40 43

  
41 44

  
42 45
logger = logging.getLogger(__name__)
......
612 615

  
613 616
logged_in = never_cache(LoggedInView.as_view())
614 617

  
618

  
619
class UserProfileJournal(ListView):
620
    model = Reference
621

  
622
    def get_queryset(self):
623
        return self.model.objects.filter(
624
            target_id=self.request.user.pk,
625
            target_ct=ContentType.objects.get_for_model(get_user_model()))
626

  
627
    def get_template_names(self):
628
        return [
629
            'profiles/user_profile_journal.html',
630
            'authentic2/user_profile_journal.html',
631
        ]
632

  
633
user_profile_journal = login_required(UserProfileJournal.as_view())
634

  
635

  
615 636
def csrf_failure_view(request, reason=""):
616 637
    messages.warning(request, _('The page is out of date, it was reloaded for you'))
617 638
    return HttpResponseRedirect(request.get_full_path())
src/authentic2_journal/__init__.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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
class Plugin(object):
18
    def get_apps(self):
19
        return [__name__]
20

  
21
default_app_config = 'authentic2_journal.apps.Authentic2JournalConfig'
src/authentic2_journal/app_settings.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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
class AppSettings(object):
18
    __DEFAULTS = {
19
            'ENABLE': False,
20
    }
21

  
22
    def __init__(self, prefix):
23
        self.prefix = prefix
24

  
25
    def _setting(self, name, dflt):
26
        from django.conf import settings
27
        return getattr(settings, self.prefix + name, dflt)
28

  
29
    def __getattr__(self, name):
30
        if name not in self.__DEFAULTS:
31
            raise AttributeError(name)
32
        return self._setting(name, self.__DEFAULTS[name])
33

  
34

  
35
import sys
36
app_settings = AppSettings('A2_JOURNAL_')
37
app_settings.__name__ = __name__
38
sys.modules[__name__] = app_settings
src/authentic2_journal/apps.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 __future__ import unicode_literals
18

  
19
from django.db import DEFAULT_DB_ALIAS, router
20
from django.apps import AppConfig
21

  
22

  
23
class Authentic2JournalConfig(AppConfig):
24
    name = 'authentic2_journal'
25
    verbose_name = 'Authentic 2 Journal Application'
26

  
27
    def ready(self):
28
        from django.db.models.signals import post_migrate
29

  
30
        post_migrate.connect(
31
            self.create_event_kinds,
32
            sender=self)
33

  
34
    def create_event_kinds(self, app_config, verbosity=2, interactive=True,
35
            using=DEFAULT_DB_ALIAS, **kwargs):
36
        from authentic2_journal.models import EventKind, KINDS
37

  
38
        if not router.allow_migrate(using, EventKind):
39
            return
40
        if EventKind.objects.filter(name__in=KINDS).count() == 9:
41
            return
42

  
43
        for kind in KINDS:
44
            EventKind.objects.get_or_create(name=kind)
src/authentic2_journal/event_logger.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 authentic2_journal.models import Line
18
from authentic2_journal.utils import new_line, new_reference, log_event
19
from django.utils.translation import ugettext_lazy as _
20

  
21

  
22
class EventLogger(object):
23
    def sso(self, user, service):
24
        content = ('action_sso', '%s' % user.email, '%s' % service)
25
        log_event(user, service, 'action_sso', content)
26

  
27

  
28
    def login(self, user, provider):
29
        content = ('action_login', '%s' % user.email, '%s' % provider)
30
        log_event(user, provider, 'action_login', content)
31

  
32

  
33
    def logout(self, user, provider):
34
        content = ('action_logout', '%s' % user.email, '%s' % provider)
35
        log_event(user, provider, 'action_logout', content)
36

  
37

  
38
    def register(self, user, ou):
39
        content = ('action_register', '%s' % user.email, '%s' % ou.slug)
40
        log_event(user, ou, 'action_register', content)
41

  
42

  
43
    def create(self, user, obj):
44
        content = ('modification_create', '%s' % user.email, '%s' % obj)
45
        log_event(user, obj, 'modification_create', content)
46

  
47

  
48
    def update(self, user, obj):
49
        content = ('modification_update', '%s' % user.email, '%s' % obj)
50
        log_event(user, obj, 'modification_update', content)
51

  
52

  
53
    def modify_members(self, user, role):
54
        content = ('modification_modify_members', '%s' % user.email,
55
                '%s' % role)
56
        log_event(user, role, 'modification_modify_members', content)
57

  
58

  
59
    def delete(self, user, obj):
60
        content = ('modification_delete', '%s' % user.email, '%s' % obj)
61
        log_event(user, obj, 'modification_delete', content)
src/authentic2_journal/models.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 __future__ import unicode_literals
18
from django.db import models
19
try:
20
    from django.contrib.contenttypes.fields import GenericForeignKey
21
except ImportError:
22
    from django.contrib.contenttypes.generic import GenericForeignKey
23
try:
24
    from django.contrib.postgres.fields import JSONField # dj1.11 native JSON
25
except:
26
    from jsonfield import JSONField # django-jsonfield outer module
27
from django.contrib.contenttypes.models import ContentType
28
from django.utils.translation import ugettext_lazy as _
29

  
30

  
31
KINDS = (
32
    'action_register',
33
    'action_login',
34
    'action_sso',
35
    'action_logout',
36
    'modification_create',
37
    'modification_update',
38
    'modification_modify_members',
39
    'modification_delete',
40
    'generic_event'
41
)
42

  
43

  
44
KIND_REPRESENTATIONS = (
45
    _('REGISTER: user %s in OU %s'),
46
    _('SSO: user %s on service %s'),
47
    _('LOGIN: user %s on provider %s'),
48
    _('LOGOUT: user %s on provider %s'),
49
    _('CREATE: user %s on %s'),
50
    _('UPDATE: user %s on %s'),
51
    _('MODIFY MEMBERS: user %s on role %s'),
52
    _('DELETE: user %s on %s'),
53
    _('GENERIC EVENT: user %s on %s')
54
)
55

  
56

  
57
def get_choices():
58
    return tuple((kind, kind) for kind in KINDS)
59

  
60

  
61
class EventKind(models.Model):
62
    name = models.CharField(
63
            verbose_name=_('name'), choices=get_choices(), max_length=256)
64

  
65

  
66
class Line(models.Model):
67
    timestamp = models.DateTimeField(auto_now=True, verbose_name=_('timestamp'))
68
    kind = models.ForeignKey(EventKind)
69
    extra = JSONField(blank=True, null=True, verbose_name=_('content'))
70

  
71

  
72
class Reference(models.Model):
73
    timestamp = models.DateTimeField(verbose_name=_('timestamp'))
74
    line = models.ForeignKey(Line)
75
    content = JSONField(blank=False, null=False, verbose_name=_('content'))
76
    target_ct = models.ForeignKey(ContentType)
77
    target_id = models.IntegerField(default=0)
78
    target = GenericForeignKey('target_ct', 'target_id')
79

  
80
    def __init__(self, *args, **kwargs):
81
        line = kwargs.get('line')
82
        if line:
83
            kwargs.update({'timestamp': line.timestamp})
84
        models.Model.__init__(self, *args, **kwargs)
src/authentic2_journal/tables.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 django_tables2 as tables
18
from authentic2_journal.models import Reference, KINDS, KIND_REPRESENTATIONS
19
from django.utils.translation import ugettext_lazy as _
20

  
21

  
22
class UserEventsTable(tables.Table):
23
    def render_content(self, value):
24
        if isinstance(value, list) and len(value) >=1 :
25
            return KIND_REPRESENTATIONS[KINDS.index(value[0])] % \
26
                    tuple(value[1:])
27

  
28
    class Meta:
29
        attrs = {'class': 'main'}
30
        model = Reference
31
        exclude = ['line', 'target_id', 'target_ct']
32
        empty_text = _('No journal entry yet.')
src/authentic2_journal/templatetags/authentic2_journal.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 ..models import KINDS, KIND_REPRESENTATIONS
18
from django import template
19

  
20
register = template.Library()
21

  
22
@register.filter
23
def to_representation(reference_content):
24
    if isinstance(reference_content, list) and len(reference_content) >=1 :
25
        return KIND_REPRESENTATIONS[KINDS.index(reference_content[0])] % \
26
                tuple(reference_content[1:])
src/authentic2_journal/utils.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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

  
19
from authentic2_journal.models import EventKind, Line, Reference
20
from django.contrib.contenttypes.models import ContentType
21
from django.core.exceptions import ObjectDoesNotExist
22

  
23

  
24
def new_line(user, obj, kind='generic_event', extra={}):
25
    logger = logging.getLogger(__name__)
26
    try:
27
        kind = EventKind.objects.get(name=kind)
28
    except:
29
        logger.error('EventKind retrieval from model failed for event name %s' %
30
            kind)
31
    else:
32
        try:
33
            line = Line.objects.create(
34
                kind=kind,
35
                extra=extra
36
            )
37
        except:
38
            logger.error("Couldn't create event journal entry for user %s on "
39
                "object %s" % (user, obj))
40
        else:
41
            return line
42

  
43

  
44
def new_reference(line, obj, content):
45
    logger = logging.getLogger(__name__)
46
    try:
47
        ct = ContentType.objects.get_for_model(obj)
48
    except ObjectDoesNotExist:
49
        logger.error('ContentType retrieval from model failed for object %s' %
50
            obj)
51
        logger.warning('No reference will be created for line %s' % line)
52
    else:
53
        try:
54
            reference = Reference.objects.create(
55
                    line=line,
56
                    content=content,
57
                    target_ct=ct,
58
                    target_id=obj.pk)
59
        except:
60
            logger.error("Couldn't create event reference for line %s and "
61
                "object %s" % (line, obj))
62
        else:
63
            return reference
64

  
65

  
66
def log_event(user, obj, kind, content):
67
    extra = {
68
        'user_email': user.email,
69
        'user_pk': user.pk,
70
        'obj_representation': '%s' % obj,
71
        'kind': kind
72
    }
73
    """
74
    No atomic DB transaction here: we can afford losing references in spite of
75
    a successful line creation.
76
    However, a line creation failure will prevent any reference creation.
77
    """
78
    line = new_line(user, obj, kind, extra)
79
    if line:
80
        new_reference(line, user, content)
81
        new_reference(line, obj, content)
82

  
83

  
84
def get_event_logger_class():
85
    from authentic2 import app_settings
86
    from authentic2_journal.event_logger import EventLogger
87

  
88
    return app_settings.A2_JOURNAL_EVENT_LOGGER or EventLogger
src/authentic2_journal/views.py
1
# authentic - Versatile identity management server
2
# Copyright (C) 2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have 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 authentic2.manager.views import SimpleSubTableView
18
from authentic2_journal.models import Reference
19
from django.contrib.auth import get_user_model
20
from django.contrib.contenttypes.models import ContentType
21

  
22

  
23
class ManagerBaseJournal(SimpleSubTableView):
24
    model = None
25
    table_class = None # TODO generic table class?
26
    template_name = '' # TODO generic template name?
27
    permissions = [] # TODO generic custom_user.view_object permission?
28
    filter_table_by_perm = False
29

  
30
    def get_table_queryset(self):
31
        return Reference.objects.filter(
32
                target_id=self.object.pk,
33
                target_ct=ContentType.objects.get_for_model(self.object))
34

  
35

  
tests/conftest.py
344 344
@pytest.fixture
345 345
def media(settings, tmpdir):
346 346
    settings.MEDIA_ROOT = str(tmpdir.mkdir('media'))
347

  
348
@pytest.fixture
349
def event_logger(db):
350
    from authentic2_journal.event_logger import EventLogger
351
    return EventLogger()
tests/test_journal.py
1
# -*- coding: utf-8 -*-
2

  
3
from authentic2.a2_rbac.models import Role
4
from authentic2.a2_rbac.utils import get_default_ou
5
from authentic2.models import Service
6
from authentic2.saml.models import (LibertyServiceProvider as SP,
7
    LibertyProvider as IdP)
8
from authentic2_journal.models import Line, Reference
9
from django.contrib.auth import get_user_model
10
from django.contrib.contenttypes.models import ContentType
11
from django.db import transaction
12
from django_rbac.utils import get_ou_model, get_role_model
13
from utils import login
14

  
15

  
16
def test_action_events(db, event_logger, app, admin, simple_user, ou1):
17
    s1 = Service.objects.create(name='s1', slug='s1')
18
    idp1 = IdP.objects.create(
19
            name='idp',
20
            slug='idp',
21
            protocol_conformance=3)
22
    sp1 = SP.objects.create(liberty_provider=idp1, enabled=True)
23
    user_ct = ContentType.objects.get_for_model(get_user_model())
24
    ou_ct = ContentType.objects.get_for_model(get_ou_model())
25
    idp_ct = ContentType.objects.get_for_model(IdP)
26
    sp_ct = ContentType.objects.get_for_model(SP)
27

  
28
    assert not len(Line.objects.all())
29
    assert not len(Reference.objects.all())
30

  
31
    event_logger.register(admin, ou1)
32
    assert len(Line.objects.all()) == 1
33
    assert len(Reference.objects.filter(
34
            target_id=admin.pk, target_ct=user_ct)) == 1
35
    assert len(Reference.objects.filter(
36
            target_id=ou1.pk, target_ct=ou_ct)) == 1
37

  
38
    event_logger.sso(admin, sp1)
39
    assert len(Line.objects.all()) == 2
40
    assert len(Reference.objects.filter(
41
            target_id=admin.pk, target_ct=user_ct)) == 2
42
    assert len(Reference.objects.filter(
43
            target_id=sp1.pk, target_ct=sp_ct)) == 1
44

  
45
    event_logger.login(admin, idp1)
46
    assert len(Line.objects.all()) == 3
47
    assert len(Reference.objects.filter(
48
            target_id=admin.pk, target_ct=user_ct)) == 3
49
    assert len(Reference.objects.filter(
50
            target_id=idp1.pk, target_ct=idp_ct)) == 1
51

  
52
    event_logger.logout(admin, idp1)
53
    assert len(Line.objects.all()) == 4
54
    assert len(Reference.objects.filter(
55
            target_id=admin.pk, target_ct=user_ct)) == 4
56
    assert len(Reference.objects.filter(
57
            target_id=idp1.pk, target_ct=idp_ct)) == 2
58

  
59

  
60
def test_modification_events(db, event_logger, admin, simple_user):
61
    r1 = Role.objects.create(name='r1', slug='r1')
62
    user_ct = ContentType.objects.get_for_model(get_user_model())
63
    ou_ct = ContentType.objects.get_for_model(get_ou_model())
64
    role_ct = ContentType.objects.get_for_model(get_role_model())
65

  
66
    assert not len(Line.objects.all())
67
    assert not len(Reference.objects.all())
68

  
69
    event_logger.create(admin, simple_user)
70
    assert len(Line.objects.all()) == 1
71
    assert len(Reference.objects.filter(
72
            target_id=admin.pk, target_ct=user_ct)) == 1
73
    assert len(Reference.objects.filter(
74
            target_id=simple_user.pk, target_ct=user_ct)) == 1
75

  
76
    event_logger.update(admin, simple_user)
77
    assert len(Line.objects.all()) == 2
78
    assert len(Reference.objects.filter(
79
            target_id=admin.pk, target_ct=user_ct)) == 2
80
    assert len(Reference.objects.filter(
81
            target_id=simple_user.pk, target_ct=user_ct)) == 2
82

  
83
    event_logger.delete(admin, simple_user)
84
    assert len(Line.objects.all()) == 3
85
    assert len(Reference.objects.filter(
86
            target_id=admin.pk, target_ct=user_ct)) == 3
87
    assert len(Reference.objects.filter(
88
            target_id=simple_user.pk, target_ct=user_ct)) == 3
89

  
90
    event_logger.modify_members(admin, r1)
91
    assert len(Line.objects.all()) == 4
92
    assert len(Reference.objects.filter(
93
            target_id=admin.pk, target_ct=user_ct)) == 4
94
    assert len(Reference.objects.filter(
95
            target_id=r1.pk, target_ct=role_ct)) == 1
96

  
97

  
98
def test_entries_timestamp_consistency(db, event_logger, admin):
99
    r1 = Role.objects.create(name='r1', slug='r1')
100
    event_logger.modify_members(admin, r1)
101
    line = Line.objects.first()
102
    for ref in Reference.objects.all():
103
        assert ref.timestamp == line.timestamp
104

  
105

  
106
def test_line_retrieval_after_object_deletion(db, event_logger):
107
    email = 'toto@nowhere.null'
108
    role_name = role_slug = 'r1'
109

  
110
    entries_role = []
111
    entries_user = []
112
    for line in Line.objects.all():
113
        if role_name in line.extra['obj_representation']:
114
            entries_role.append(line)
115
        if email == line.extra['user_email']:
116
            entries_user.append(line)
117
    assert not len(entries_role)
118
    assert not len(entries_user)
119

  
120
    role = Role.objects.create(name=role_name, slug=role_slug)
121
    user = get_user_model().objects.create(email=email)
122
    event_logger.modify_members(user, role)
123
    user.delete()
124
    role.delete()
125

  
126
    entries_role = []
127
    entries_user = []
128
    for line in Line.objects.all():
129
        if role_name in line.extra['obj_representation']:
130
            entries_role.append(line)
131
        if email == line.extra['user_email']:
132
            entries_user.append(line)
133
    assert len(entries_role)
134
    assert len(entries_user)
135

  
136

  
137
def test_reference_retrieval_after_objects_deletion(db, event_logger):
138
    email = 'toto@nowhere.null'
139
    role_name = role_slug = 'r1'
140

  
141
    entries_role = []
142
    entries_user = []
143
    for ref in Reference.objects.all():
144
        if role_name in ref.content:
145
            entries_role.append(ref)
146
        if email in ref.content:
147
            entries_user.append(ref)
148
    assert not len(entries_role)
149
    assert not len(entries_user)
150

  
151
    role = Role.objects.create(name=role_name, slug=role_slug)
152
    user = get_user_model().objects.create(email=email)
153
    event_logger.modify_members(user, role)
154
    user.delete()
155
    role.delete()
156

  
157
    entries_role = []
158
    entries_user = []
159
    for ref in Reference.objects.all():
160
        if role_name in ref.content:
161
            entries_role.append(ref)
162
        if email in ref.content:
163
            entries_user.append(ref)
164
    assert len(entries_role)
165
    assert len(entries_user)
166

  
167

  
168
def test_backoffice_user_journal_view(db, event_logger, app, admin, simple_user, ou1):
169
    login(app, admin, '/manage/')
170
    url = u'/manage/users/%s/journal/' % admin.pk
171

  
172
    event_logger.create(admin, simple_user)
173
    event_logger.create(admin, ou1)
174

  
175
    response = app.get(url, status=200)
176
    table_lines = response.html.find(
177
            'div', attrs={'class': 'table-container'}).find(
178
            'table', 'main').find('tbody').find_all('tr')
179
    assert len(table_lines) == 2
180

  
181
    # assumptions on table content
182
    for line in table_lines:
183
        assert 'CREATE: user %s' % admin.email in line.text
184
        if 'on user' in line.text:
185
            assert 'on %s' % simple_user in line.text
186
        else:
187
            assert 'on %s' % ou1 in line.text
188

  
189

  
190
def test_accounts_user_journal_view(db, event_logger, app, admin, simple_user, ou1):
191
    login(app, admin, '/accounts/')
192
    url = u'/accounts/journal/'
193

  
194
    event_logger.create(admin, simple_user)
195
    event_logger.create(admin, ou1)
196

  
197
    response = app.get(url, status=200)
198
    ref_list = response.html.find('div', attrs={'id': 'content'}).find('ul')
199
    assert len(ref_list.find_all('li')) == 2
200

  
201
    # assumptions on table content
202
    for entry in ref_list.find_all('li'):
203
        assert 'CREATE: user %s' % admin.email in entry.text
204
        if 'on user' in entry.text:
205
            assert 'on %s' % simple_user in entry.text
206
        else:
207
            assert 'on %s' % ou1 in entry.text
208

  
209

  
210
def test_backoffice_unauthorized_access(db, event_logger, app, admin, simple_user, ou1):
211
    event_logger.create(admin, ou1)
212
    event_logger.update(admin, ou1)
213

  
214
    login(app, simple_user, '/')
215
    url = u'/manage/users/%s/journal/' % admin.pk
216
    response = app.get(url, status=403)
0
-