Projet

Général

Profil

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

Paul Marillonnet, 18 décembre 2018 17:05

Télécharger (40 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      |  22 ++
 src/authentic2/views.py                       |  42 +++-
 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               | 104 +++++++++
 src/authentic2_journal/logger.py              |  21 ++
 src/authentic2_journal/migrations/__init__.py |   0
 src/authentic2_journal/models.py              |  71 ++++++
 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                         | 205 ++++++++++++++++++
 33 files changed, 705 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
54 54
        url(r'^users/uuid:(?P<slug>[a-z0-9]+)/change-email/$',
55 55
            user_views.user_change_email,
56 56
            name='a2-manager-user-by-uuid-change-email'),
57
        url('^users/(?P<pk>\d+)/journal/$',
58
            user_views.user_journal,
59
            name='a2-manager-user-journal'),
57 60

  
58 61
        # Authentic2 roles
59 62
        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

  
......
518 520

  
519 521

  
520 522
user_delete = UserDeleteView.as_view()
523

  
524

  
525
class UserJournal(ManagerBaseJournal):
526
    model = get_user_model()
527
    table_class = UserEventsTable
528
    template_name = 'authentic2/manager/user_journal.html'
529
    permissions = ['custom_user.view_user']
530
    filter_table_by_perm = False
531

  
532

  
533
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
124 124
    'authentic2.disco_service',
125 125
    'authentic2.manager',
126 126
    'authentic2_provisionning_ldap',
127
    'authentic2_journal',
127 128
    'authentic2',
128 129
    'django_rbac',
129 130
    '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
  {% block pagination %}
15
    {% include "gadjo/pagination.html" %}
16
  {% endblock %}
17
  <form action="{% url 'user-profile-journal' %}" method="get">
18
    {% csrf_token %}
19
    {{ form }}
20
    <input type="submit" value="Submit">
21
  </form>
22
{% 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__)
......
613 617

  
614 618
logged_in = never_cache(LoggedInView.as_view())
615 619

  
620

  
621
class UserProfileJournal(ListView, FormView):
622
    model = Line
623
    form_class = forms.EventTimewindowForm
624
    paginate_by = 10
625

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

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

  
653
user_profile_journal = login_required(UserProfileJournal.as_view())
654

  
655

  
616 656
def csrf_failure_view(request, reason=""):
617 657
    messages.warning(request, _('The page is out of date, it was reloaded for you'))
618 658
    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
from django.contrib.contenttypes.models import ContentType
18
from django.template.loader import render_to_string, select_template
19

  
20

  
21
_kind_registry = {}
22

  
23

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

  
32

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

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

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

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

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

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

  
66

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

  
71

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

  
76

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

  
81

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

  
86

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

  
91

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

  
96

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

  
101

  
102
class DeleteKind(BaseKind):
103
    name = 'delete'
104
    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
from jsonfield import JSONField # django-jsonfield outer module
26
from django.contrib.contenttypes.models import ContentType
27
from django.utils.translation import ugettext_lazy as _
28

  
29

  
30
class Kind(models.Model):
31
    name = models.CharField(max_length=128, null=True, verbose_name='name')
32

  
33
    @property
34
    def concrete(self):
35
        return kinds._kind_registry[self.name]
36

  
37

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

  
43

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

  
49
    def format(self):
50
        return self.kind.concrete().format(self)
51

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

  
55

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

  
64
    def __init__(self, *args, **kwargs):
65
        line = kwargs.get('line')
66
        if line:
67
            kwargs.update({
68
                'timestamp': line.timestamp,
69
                'extra_json': line.extra_json
70
            })
71
        return super(Reference, self).__init__(*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 line.extra_json['obj']:
122
            entries_role.append(line)
123
        if line.extra_json['user'].startswith(email):
124
            entries_user.append(line)
125
    assert len(entries_role)
126
    assert len(entries_user)
127

  
128

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

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

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

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

  
151

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

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

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

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

  
171

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

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

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

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

  
189

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

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

  
198

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