Projet

Général

Profil

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

Paul Marillonnet, 05 décembre 2018 17:15

Télécharger (40,1 ko)

Voir les différences:

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

 MANIFEST.in                                   |   1 +
 setup.py                                      |   1 +
 src/authentic2/app_settings.py                |   1 +
 src/authentic2/forms/__init__.py              |   6 +
 .../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      |  19 ++
 src/authentic2/views.py                       |  41 +++-
 src/authentic2_journal/__init__.py            |  21 ++
 src/authentic2_journal/admin.py               |   0
 src/authentic2_journal/app_settings.py        |  38 ++++
 src/authentic2_journal/apps.py                |  25 +++
 src/authentic2_journal/kinds.py               | 106 +++++++++
 src/authentic2_journal/logger.py              |  21 ++
 src/authentic2_journal/migrations/__init__.py |   0
 src/authentic2_journal/models.py              |  74 +++++++
 src/authentic2_journal/tables.py              |  35 +++
 .../templates/authentic2_journal/create.html  |   2 +
 .../templates/authentic2_journal/delete.html  |   2 +
 .../templates/authentic2_journal/login.html   |   2 +
 .../templates/authentic2_journal/logout.html  |   2 +
 .../authentic2_journal/modify_members.html    |   2 +
 .../authentic2_journal/register.html          |   2 +
 .../templates/authentic2_journal/sso.html     |   2 +
 .../templates/authentic2_journal/update.html  |   2 +
 .../templatetags/__init__.py                  |   0
 .../templatetags/authentic2_journal.py        |  27 +++
 src/authentic2_journal/views.py               |  34 +++
 tests/conftest.py                             |   6 +
 tests/test_journal.py                         | 206 ++++++++++++++++++
 33 files changed, 707 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/kinds.py
 create mode 100644 src/authentic2_journal/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/templates/authentic2_journal/create.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/delete.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/login.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/logout.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/modify_members.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/register.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/sso.html
 create mode 100644 src/authentic2_journal/templates/authentic2_journal/update.html
 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/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/forms/__init__.py
8 8

  
9 9
from authentic2.compat import get_user_model
10 10
from authentic2.forms.fields import PasswordField
11
from authentic2.forms.widgets import DateTimeWidget
11 12

  
12 13
from .. import app_settings
13 14
from ..exponential_retry_timeout import ExponentialRetryTimeout
......
220 221

  
221 222
class SiteImportForm(forms.Form):
222 223
    site_json = forms.FileField(label=_('Site Export File'))
224

  
225

  
226
class EventTimewindowForm(forms.Form):
227
    after = forms.DateTimeField(label=_('After'), widget=DateTimeWidget)
228
    before = forms.DateTimeField(label=_('Before'), widget=DateTimeWidget)
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 line in object_list %}
9
      <li>{{ line.timestamp }} - {{ line|to_representation }}</li>
10
  {% empty %}
11
      <li>{% trans "No journal entry yet." %}</li>
12
  {% endfor %}
13
  </ul>
14
  <form action="{% url 'user-profile-journal' %}" method="get">
15
    {% csrf_token %}
16
    {{ form }}
17
    <input type="submit" value="Submit">
18
  </form>
19
{% endblock %}
src/authentic2/views.py
7 7
import collections
8 8

  
9 9

  
10
from datetime import datetime
10 11
from django.conf import settings
11 12
from django.shortcuts import render_to_response, render
12 13
from django.template.loader import render_to_string, select_template
13 14
from django.views.generic.edit import UpdateView, FormView
14 15
from django.views.generic import RedirectView, TemplateView
15 16
from django.views.generic.base import View
17
from django.views.generic.list import ListView
16 18
from django.contrib.auth import SESSION_KEY
17 19
from django import http, shortcuts
18 20
from django.core import mail, signing
......
21 23
from django.contrib import messages
22 24
from django.utils.translation import ugettext as _
23 25
from django.contrib.auth import logout as auth_logout
24
from django.contrib.auth import REDIRECT_FIELD_NAME
26
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
27
from django.contrib.contenttypes.models import ContentType
25 28
from django.http import (HttpResponseRedirect, HttpResponseForbidden,
26 29
    HttpResponse)
27 30
from django.core.exceptions import PermissionDenied
......
37 40

  
38 41

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

  
41 45

  
42 46
logger = logging.getLogger(__name__)
......
612 616

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

  
619

  
620
class UserProfileJournal(ListView, FormView):
621
    model = Line
622
    form_class = forms.EventTimewindowForm
623

  
624
    def get_queryset(self):
625
        timewindow = {}
626
        for qs_param, qs_suffix in {'after': 'gte', 'before': 'lte'}.items():
627
            delimiter = self.request.GET.get(qs_param)
628
            if delimiter:
629
                try:
630
                    dt = datetime.strptime(delimiter, '%d/%m/%Y %H:%M:%S')
631
                except:
632
                    logger.info('UserProfileJournal wrong %s timestamp query '
633
                            'string parameter: %s' % (qs_param, delimiter))
634
                else:
635
                    timewindow.update({'timestamp__%s' % qs_suffix: dt})
636
        """
637
        Timestamp consistency allows for time window filtering on Reference
638
        objects directly
639
        """
640
        return [ref.line for ref in Reference.objects.filter(
641
            target_id=self.request.user.pk,
642
            target_ct=ContentType.objects.get_for_model(get_user_model()),
643
            **timewindow)]
644

  
645
    def get_template_names(self):
646
        return [
647
            'profiles/user_profile_journal.html',
648
            'authentic2/user_profile_journal.html',
649
        ]
650

  
651
user_profile_journal = login_required(UserProfileJournal.as_view())
652

  
653

  
615 654
def csrf_failure_view(request, reason=""):
616 655
    messages.warning(request, _('The page is out of date, it was reloaded for you'))
617 656
    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'
src/authentic2_journal/kinds.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 json
18

  
19
from django.contrib.contenttypes.models import ContentType
20
from django.template.loader import render_to_string, select_template
21

  
22

  
23
_kind_registry = {}
24

  
25

  
26
class KindMetaclass(type):
27
    def __init__(self, name, bases, dct):
28
        super(KindMetaclass, self).__init__(name, bases, dct)
29
        if name != "BaseKind":
30
            assert set(dct.keys()) >= set(['name', 'template_names'])
31
            assert dct['name'] not in _kind_registry
32
            _kind_registry[dct['name']] = self
33

  
34

  
35
class BaseKind(object):
36
    __metaclass__ = KindMetaclass
37
    long_template_names = []
38
    template_names = []
39

  
40
    def format(self, line):
41
        return select_template(self.template_names).render(context=line.extra_json)
42

  
43
    def long_format(self, line):
44
        if not self.long_template_names:
45
            return ''
46
        return select_template(self.long_template_names).render(context=line.extra_json)
47

  
48
    def log_real(self, **kwargs):
49
        from authentic2_journal.models import Line, get_kind
50
        kind = get_kind(self.name)
51
        if isinstance(kind, tuple):
52
            kind = kind[0]
53
        return Line.objects.create(kind=kind, extra_json=kwargs)
54

  
55
    def create_reference(self, line, obj, **kwargs):
56
        from authentic2_journal.models import Reference
57
        reference = Reference.objects.create(
58
                line=line,
59
                target_ct=ContentType.objects.get_for_model(obj),
60
                target_id=obj.pk)
61

  
62
    def log(self, user, obj):
63
        line = self.log_real(user_pk=user.pk, obj_pk=obj.pk, user='%s' % user, obj='%s' % obj)
64
        if line:
65
            self.create_reference(line, user)
66
            self.create_reference(line, obj)
67

  
68

  
69
class SSOKind(BaseKind):
70
    name = 'sso'
71
    template_names = ['authentic2_journal/sso.html']
72

  
73

  
74
class LoginKind(BaseKind):
75
    name = 'login'
76
    template_names = ['authentic2_journal/login.html']
77

  
78

  
79
class LogoutKind(BaseKind):
80
    name = 'logout'
81
    template_names = ['authentic2_journal/logout.html']
82

  
83

  
84
class RegisterKind(BaseKind):
85
    name = 'register'
86
    template_names = ['authentic2_journal/register.html']
87

  
88

  
89
class CreateKind(BaseKind):
90
    name = 'create'
91
    template_names = ['authentic2_journal/create.html']
92

  
93

  
94
class UpdateKind(BaseKind):
95
    name = 'update'
96
    template_names = ['authentic2_journal/update.html']
97

  
98

  
99
class ModifyMembersKind(BaseKind):
100
    name = 'modify_members'
101
    template_names = ['authentic2_journal/modify_members.html']
102

  
103

  
104
class DeleteKind(BaseKind):
105
    name = 'delete'
106
    template_names = ['authentic2_journal/delete.html']
src/authentic2_journal/logger.py
1
from authentic2_journal import kinds, models
2

  
3

  
4
class Logger(object):
5
    def __init__(self, kind):
6
        self.kind = kinds._kind_registry[kind]
7

  
8
    def __call__(self, *args, **kwargs):
9
        kind_model = models.get_kind(self.kind.name)
10
        if isinstance(kind_model, tuple):
11
            kind_model = kind_model[0]
12
        kind = kind_model.concrete()
13
        return self.kind.log(kind, *args, **kwargs)
14

  
15

  
16
class LoggerHub(object):
17
    def __getattr__(self, kind_name):
18
        return Logger(kind_name)
19

  
20

  
21
logger = LoggerHub()
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 authentic2.decorators import GlobalCache
19
from authentic2_journal import kinds
20
from django.db import models
21
try:
22
    from django.contrib.contenttypes.fields import GenericForeignKey
23
except ImportError:
24
    from django.contrib.contenttypes.generic import GenericForeignKey
25
try:
26
    from django.contrib.postgres.fields import JSONField # dj1.11 native JSON
27
except:
28
    from jsonfield import JSONField # django-jsonfield outer module
29
from django.contrib.contenttypes.models import ContentType
30
from django.utils.translation import ugettext_lazy as _
31

  
32

  
33
class Kind(models.Model):
34
    name = models.CharField(max_length=128, null=True, verbose_name='name')
35

  
36
    @property
37
    def concrete(self):
38
        return kinds._kind_registry[self.name]
39

  
40

  
41
# get_kind() should be cached, like ContentType are, because they never vary
42
@GlobalCache
43
def get_kind(name):
44
    return Kind.objects.get_or_create(name=name)
45

  
46

  
47
class Line(models.Model):
48
    timestamp = models.DateTimeField(auto_now=True, verbose_name=_('timestamp'))
49
    kind = models.ForeignKey(Kind)
50
    extra_json = JSONField(blank=True, null=True, verbose_name=_('extra JSON'))
51

  
52
    def format(self):
53
        return self.kind.concrete().format(self)
54

  
55
    def long_format(self):
56
        return self.kind.concrete().long_format(self)
57

  
58

  
59
class Reference(models.Model):
60
    timestamp = models.DateTimeField(verbose_name=_('timestamp'))
61
    line = models.ForeignKey(Line)
62
    extra_json = JSONField(blank=False, null=False, verbose_name=_('additional description'))
63
    target_ct = models.ForeignKey(ContentType)
64
    target_id = models.IntegerField(default=0)
65
    target = GenericForeignKey('target_ct', 'target_id')
66

  
67
    def __init__(self, *args, **kwargs):
68
        line = kwargs.get('line')
69
        if line:
70
            kwargs.update({
71
                'timestamp': line.timestamp,
72
                'extra_json': line.extra_json
73
            })
74
        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

  
19
from authentic2_journal.models import Line
20
from django.utils.translation import ugettext_lazy as _
21

  
22

  
23
class UserEventsTable(tables.Table):
24
    def render_kind(self, value):
25
        return value.name
26

  
27
    def render_extra_json(self, value, record):
28
        return record.format() or value
29

  
30
    class Meta:
31
        attrs = {'class': 'main'}
32
        model = Line
33
        order_by = ('-timestamp', 'id')
34
        per_page = 10
35
        empty_text = _('No journal entry yet.')
src/authentic2_journal/templates/authentic2_journal/create.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed create on {{obj}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/delete.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed delete on {{obj}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/login.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed login on {{provider}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/logout.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed logout on {{provider}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/modify_members.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed modify members on {{role}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/register.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} registered on {{ou}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/sso.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed sso on {{service}}{% endblocktrans %}
src/authentic2_journal/templates/authentic2_journal/update.html
1
{% load i18n %}
2
{% blocktrans %}{{user}} performed update on {{obj}}{% endblocktrans %}
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 django import template
18

  
19
register = template.Library()
20

  
21
@register.filter
22
def to_representation(line):
23
    return line.format()
24

  
25
@register.filter
26
def to_long_representation(line):
27
    return line.long_format()
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
    table_pagination = True
30

  
31
    def get_table_queryset(self):
32
        return [ref.line for ref in Reference.objects.filter(
33
                target_id=self.object.pk,
34
                target_ct=ContentType.objects.get_for_model(self.object))]
tests/conftest.py
344 344
@pytest.fixture
345 345
def media(settings, tmpdir):
346 346
    settings.MEDIA_ROOT = str(tmpdir.mkdir('media'))
347

  
348

  
349
@pytest.fixture
350
def event_logger(db):
351
    from authentic2_journal.logger import LoggerHub
352
    return LoggerHub()
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 = u'toto@nowhere.null'
108
    role_name = role_slug = u'r1'
109

  
110
    assert not len(Line.objects.all())
111

  
112
    role = Role.objects.create(name=role_name, slug=role_slug)
113
    user = get_user_model().objects.create(email=email)
114
    event_logger.modify_members(user, role)
115
    user.delete()
116
    role.delete()
117

  
118
    entries_role = []
119
    entries_user = []
120
    for line in Line.objects.all():
121
        # if role_name in json.loads(line.extra_json)['obj']:
122
        if role_name in line.extra_json['obj']:
123
            entries_role.append(line)
124
        if line.extra_json['user'].startswith(email):
125
            entries_user.append(line)
126
    assert len(entries_role)
127
    assert len(entries_user)
128

  
129

  
130
def test_reference_retrieval_after_objects_deletion(db, event_logger):
131
    email = 'toto@nowhere.null'
132
    role_name = role_slug = 'r1'
133

  
134
    assert not(Reference.objects.all())
135

  
136
    role = Role.objects.create(name=role_name, slug=role_slug)
137
    user = get_user_model().objects.create(email=email)
138
    event_logger.modify_members(user, role)
139
    user.delete()
140
    role.delete()
141

  
142
    entries_role = []
143
    entries_user = []
144
    for ref in Reference.objects.all():
145
        if role_name in ref.extra_json['obj']:
146
            entries_role.append(ref)
147
        if ref.extra_json.get('user', '').startswith(email):
148
            entries_user.append(ref)
149
    assert len(entries_role)
150
    assert len(entries_user)
151

  
152

  
153
def test_backoffice_user_journal_view(db, event_logger, app, admin, simple_user, ou1):
154
    login(app, admin, '/manage/')
155
    url = u'/manage/users/%s/journal/' % admin.pk
156

  
157
    event_logger.create(admin, simple_user)
158
    event_logger.create(admin, ou1)
159

  
160
    response = app.get(url, status=200)
161
    table_lines = response.html.find(
162
            'div', attrs={'class': 'table-container'}).find(
163
            'table', 'main').find('tbody').find_all('tr')
164
    assert len(table_lines) == 2
165

  
166
    # assumptions on table content
167
    for line in table_lines:
168
        assert 'create' in line.text
169
        assert '%s' % admin in line.text
170
        assert '%s' % simple_user in line.text or '%s' % ou1 in line.text
171

  
172

  
173
def test_accounts_user_journal_view(db, event_logger, app, admin, simple_user, ou1):
174
    login(app, admin, '/accounts/')
175
    url = u'/accounts/journal/'
176

  
177
    event_logger.create(admin, simple_user)
178
    event_logger.create(admin, ou1)
179

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

  
184
    # assumptions on table content
185
    for entry in ref_list.find_all('li'):
186
        assert 'create' in entry.text
187
        assert '%s' % admin in entry.text
188
        assert '%s' % simple_user in entry.text or '%s' % ou1 in entry.text
189

  
190

  
191
def test_backoffice_unauthorized_access(db, event_logger, app, admin, simple_user, ou1):
192
    event_logger.create(admin, ou1)
193
    event_logger.update(admin, ou1)
194

  
195
    login(app, simple_user, '/')
196
    url = u'/manage/users/%s/journal/' % admin.pk
197
    response = app.get(url, status=403)
198

  
199

  
200
def test_log_event_escalate_error(db, event_logger, app, admin, simple_user, ou1):
201
    try:
202
        event_logger.spoke_to(admin, simple_user)
203
    except:
204
        pass
205
    else:
206
        assert 0
0
-