Customize admin roles (#35737)

This commit is contained in:
Benjamin Dauvergne 2019-09-13 18:09:09 +02:00
parent e151061a00
commit 868ac78377
6 changed files with 366 additions and 0 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ pip-selfcheck.json
src/authentic2_wallonie_connect.egg-info/
test_py27-dj111-pg-coverage_results.xml
*.pyc
dist

View File

@ -19,3 +19,7 @@ import django.apps
class AppConfig(django.apps.AppConfig):
name = 'authentic2_wallonie_connect'
def ready(self):
from . import roles
roles.init(self)

View File

@ -0,0 +1,117 @@
# coding: utf-8
#
# authentic2-wallonie-connect - Authentic2 plugin for the Wallonie Connect usecase
# Copyright (C) 2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from django.apps import apps
from django.db import router
from django.db.models.signals import post_save, post_migrate
from django.contrib.contenttypes.models import ContentType
from django_rbac.utils import get_operation
from django_rbac.models import (
ADMIN_OP,
CHANGE_OP,
ADD_OP,
)
from authentic2.custom_user.models import User
from authentic2.a2_rbac.models import (
OrganizationalUnit as OU,
Role,
Permission,
RESET_PASSWORD_OP,
ACTIVATE_OP,
CHANGE_EMAIL_OP,
)
from authentic2.models import Service
def init(app_config):
post_migrate.connect(post_migrate_handler, sender=apps.get_app_config('a2_rbac'))
post_save.connect(ou_post_save_handler, sender=OU)
def post_migrate_handler(using, **kwargs):
if not router.allow_migrate(using, Role):
return
create_base_roles()
def create_base_roles():
admin, created = Role.objects.get_or_create(
slug='_imio-wc-admin',
ou=None,
defaults={
'name': 'Administrateur',
})
set_permissions(admin, [
(User, None, [ADMIN_OP]),
(Role, None, [ADMIN_OP]),
(Service, None, [ADMIN_OP]),
(OU, None, [ADMIN_OP]),
])
def ou_post_save_handler(instance, **kwargs):
update_ou_roles(instance)
def update_ous_roles():
for ou in OU.objects.all():
update_ou_roles(ou)
def update_ou_roles(ou):
if not ou.username_is_unique:
ou.username_is_unique = True
ou.save()
# Create admin of users
user_admin, created = Role.objects.get_or_create(
slug='_imio-wc-user-admin',
ou=ou,
defaults={
'name': 'Administrateur des utilisateurs',
})
set_permissions(user_admin, [(User, ou, [
ADD_OP, CHANGE_OP, RESET_PASSWORD_OP,
ACTIVATE_OP, CHANGE_EMAIL_OP])])
# Create admin of roles
role_admin, created = Role.objects.get_or_create(
slug='_imio-wc-role-admin',
ou=ou,
defaults={
'name': 'Administrateur des rôles',
})
set_permissions(role_admin, [(Role, ou, [CHANGE_OP])])
def set_permissions(role, model_scope_ops):
permissions = set()
for model, scope, operations in model_scope_ops:
for operation in operations:
permission, created = Permission.objects.get_or_create(
operation=get_operation(operation),
ou=scope,
target_ct=ContentType.objects.get_for_model(ContentType),
target_id=ContentType.objects.get_for_model(model).pk)
permissions.add(permission)
if set(role.permissions.all()) != permissions:
role.permissions = permissions

View File

@ -9,3 +9,8 @@ DATABASES = {
},
}
}
A2_RBAC_MANAGED_CONTENT_TYPES = ()
A2_RBAC_ROLE_ADMIN_RESTRICT_TO_OU_USERS = True
INSTALLED_APPS += ('authentic2_wallonie_connect',)

194
tests/test_acl.py Normal file
View File

@ -0,0 +1,194 @@
# coding: utf-8
#
# authentic2-wallonie-connect - Authentic2 plugin for the Wallonie Connect usecase
# Copyright (C) 2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import pytest
from authentic2.custom_user.models import User
from authentic2.a2_rbac.models import OrganizationalUnit as OU, Role
from authentic2.models import Service
from utils import login
@pytest.fixture(autouse=True)
def base(db):
class Base(object):
pass
b = Base()
OU.objects.filter(default=True).update(name='IMIO', slug='imio')
b.ou_imio = OU.objects.get(slug='imio')
b.role_global_admin = Role.objects.get(slug='_imio-wc-admin')
b.user_admin = User.objects.create(ou=b.ou_imio, username='admin')
b.user_admin.roles.add(b.role_global_admin)
b.ou_liege = OU.objects.create(name='Liège', slug='liege')
b.role_liege_user_admin = Role.objects.get(slug='_imio-wc-user-admin', ou=b.ou_liege)
b.role_liege_role_admin = Role.objects.get(slug='_imio-wc-role-admin', ou=b.ou_liege)
b.service_liege = Service.objects.create(ou=b.ou_liege, slug='service', name='Service')
b.role_service_liege = Role.objects.create(ou=b.ou_liege, slug='service', name='Accès Service')
b.service_liege.authorized_roles.through.objects.create(service=b.service_liege, role=b.role_service_liege)
b.user_liege = User.objects.create(ou=b.ou_liege, username='user-liege')
b.user_liege_admin = User.objects.create(ou=b.ou_liege, username='admin-liege')
b.user_liege_admin.roles.add(b.role_liege_user_admin)
b.user_liege_admin.roles.add(b.role_liege_role_admin)
b.user_liege_service_admin = User.objects.create(ou=b.ou_liege, username='service-admin-liege')
b.user_liege_service_admin.roles.add(b.role_service_liege.get_admin_role())
b.ou_tournay = OU.objects.create(name='Tournay', slug='tournay')
b.role_tournay_user_admin = Role.objects.get(slug='_imio-wc-user-admin', ou=b.ou_tournay)
b.role_tournay_role_admin = Role.objects.get(slug='_imio-wc-role-admin', ou=b.ou_tournay)
b.user_tournay = User.objects.create(ou=b.ou_tournay, username='user-tournay')
# set all passwords
for user in User.objects.all():
user.set_password(user.username)
user.save()
return b
def test_imio_admin(app, base):
home = login(app, base.user_admin, '/manage/')
assert (set(elt.attrib['href'].strip('/').split('/')[1] for elt in home.pyquery('.apps li a'))
== set(['organizational-units',
'roles',
'users',
'services']))
users = home.click('Users')
assert len(users.pyquery('table tbody tr')) == User.objects.count()
# Can add user in IMIO
users_imio = app.get('/manage/users/?search-ou=%s' % base.ou_imio.pk)
assert len(users_imio.pyquery('table tbody tr')) == 1
users_imio.click('Add user')
# Can add user in Liège
users_liege = app.get('/manage/users/?search-ou=%s' % base.ou_liege.pk)
assert len(users_liege.pyquery('table tbody tr')) == 3
users_liege.click('Add user')
# Can edit user in IMIO
user_imio = app.get('/manage/users/%s/' % base.user_admin.pk)
assert (set(elt.text for elt in user_imio.pyquery('.actions a:not(.disabled)'))
== set(['Delete', 'Edit']))
assert (set(elt.text for elt in user_imio.pyquery('.other_actions button'))
== set(['Suspend',
'Reset password',
'Modify roles',
'Change user password',
'Force password change on next login']))
# or Liège
user_liege = app.get('/manage/users/%s/' % base.user_liege_admin.pk)
assert (set(elt.text for elt in user_liege.pyquery('.actions a:not(.disabled)'))
== set(['Delete', 'Edit']))
assert (set(elt.text for elt in user_liege.pyquery('.other_actions button'))
== set(['Suspend',
'Reset password',
'Modify roles',
'Change user password',
'Force password change on next login']))
def test_liege_admin(app, base):
home = login(app, base.user_liege_admin, '/manage/')
assert (set(elt.attrib['href'].strip('/').split('/')[1] for elt in home.pyquery('.apps li a'))
== set(['roles', 'users']))
users = home.click('Users')
assert len(users.pyquery('table tbody tr')) == User.objects.filter(ou=base.ou_liege).count()
# Can add user directly
users.click('Add user')
# Cannot see user in IMIO or Tournay
app.get('/manage/users/%s/' % base.user_admin.pk, status=403)
app.get('/manage/users/%s/' % base.user_tournay.pk, status=403)
# Cane edit but not delete user in Liège
user_liege = app.get('/manage/users/%s/' % base.user_liege_admin.pk)
assert (set(elt.text for elt in user_liege.pyquery('.actions a:not(.disabled)'))
== set(['Edit']))
assert (set(elt.text for elt in user_liege.pyquery('.other_actions button'))
== set(['Suspend',
'Reset password',
'Modify roles',
'Force password change on next login']))
def test_liege_service_admin(app, base):
home = login(app, base.user_liege_service_admin, '/manage/')
assert (set(elt.attrib['href'].strip('/').split('/')[1] for elt in home.pyquery('.apps li a'))
== set(['roles', 'users']))
users = home.click('Users')
assert len(users.pyquery('table tbody tr')) == User.objects.filter(ou=base.ou_liege).count()
# Cannot add user
assert len(users.pyquery('#add-user-btn.disabled')) == 1
# Can see user in Liège
app.get('/manage/users/%s/edit/' % base.user_liege.pk, status=403)
# Cannot see in IMIO or Tournay
app.get('/manage/users/%s/' % base.user_admin.pk, status=403)
app.get('/manage/users/%s/' % base.user_tournay.pk, status=403)
# Cane edit but not delete user in Liège but modify roles
user_liege = app.get('/manage/users/%s/' % base.user_liege.pk)
# Can't do anthing on a user of Liège
assert (set(elt.text for elt in user_liege.pyquery('.actions a:not(.disabled)'))
== set([]))
assert (set(elt.text for elt in user_liege.pyquery('.other_actions button'))
== set(['Modify roles']))
# but modify members of the service role
modify_roles = app.get('/manage/users/%s/roles/' % base.user_liege.pk)
assert len(modify_roles.pyquery('table tbody tr td.name a')) == 1
assert modify_roles.pyquery('table tbody tr td.name a')[0].text == 'Accès Service'
assert base.user_liege.roles.count() == 0
modify_roles = app.post('/manage/users/%s/roles/' % base.user_liege.pk,
params={
'csrfmiddlewaretoken': app.cookies['csrftoken'],
'action': 'add',
'role': base.role_service_liege.pk
})
assert base.user_liege.roles.count() == 1
assert base.user_liege.roles.get().name == 'Accès Service'
roles = app.get('/manage/roles/')
assert len(roles.pyquery('table tbody tr')) == 1
assert roles.pyquery('table tbody tr td.name a')[0].text == 'Accès Service'
# Select3 only shows users from Liège
service_role = app.get('/manage/roles/%s/' % base.role_service_liege.pk)
select2_url = service_role.pyquery('select#id_user')[0].attrib['data-ajax--url']
select2_field_id = service_role.pyquery('select#id_user')[0].attrib['data-field_id']
select2_response = app.get(select2_url, params={'field_id': select2_field_id, 'term': ''})
assert select2_response.json['more'] is False
assert len(select2_response.json['results']) == 3
ids = set(result['id'] for result in select2_response.json['results'])
assert User.objects.filter(id__in=ids, ou=base.ou_liege).count() == 3

45
tests/utils.py Normal file
View File

@ -0,0 +1,45 @@
# authentic2-wallonie-connect - Authentic2 plugin for the Wallonie Connect usecase
# Copyright (C) 2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from django.core.urlresolvers import reverse
from django.shortcuts import resolve_url
def login(app, user, path=None, password=None, remember_me=None, args=None, kwargs=None):
if path:
args = args or []
kwargs = kwargs or {}
path = resolve_url(path, *args, **kwargs)
login_page = app.get(path, status=302).maybe_follow()
else:
login_page = app.get(reverse('auth_login'))
assert login_page.request.path == reverse('auth_login')
form = login_page.form
form.set('username', user.username if hasattr(user, 'username') else user)
# password is supposed to be the same as username
form.set('password', password or user.username)
if remember_me is not None:
form.set('remember_me', bool(remember_me))
response = form.submit(name='login-password-submit').follow()
if path:
assert response.request.path == path
else:
assert response.request.path == reverse('auth_homepage')
assert '_auth_user_id' in app.session
return response