wip/72703-api-client-ou-roles-list #7

Closed
pmarillonnet wants to merge 4 commits from wip/72703-api-client-ou-roles-list into wip/72688-api-client-manager-ou-permissions
5 changed files with 108 additions and 7 deletions

View File

@ -20,7 +20,7 @@ from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from authentic2.a2_rbac.models import OrganizationalUnit from authentic2.a2_rbac.models import OrganizationalUnit, Role
from authentic2.manager import forms from authentic2.manager import forms
from authentic2.manager.views import MediaMixin, PermissionMixin, TitleMixin from authentic2.manager.views import MediaMixin, PermissionMixin, TitleMixin
from authentic2.models import APIClient from authentic2.models import APIClient
@ -55,6 +55,13 @@ class APIClientsFormViewMixin(APIClientsMixin):
if self.request.user.has_ou_perm('authentic2.admin_apiclient', ou): if self.request.user.has_ou_perm('authentic2.admin_apiclient', ou):
allowed_ous.append(ou.id) allowed_ous.append(ou.id)
form.fields['ou'].queryset = OrganizationalUnit.objects.filter(id__in=allowed_ous) form.fields['ou'].queryset = OrganizationalUnit.objects.filter(id__in=allowed_ous)
form.fields['ou'].required = True
form.fields['ou'].empty_label = None
api_client = self.object
if api_client and api_client.ou is not None:
form.fields['apiclient_roles'].queryset = Role.objects.filter(ou=api_client.ou).exclude(
slug__startswith='_'
)
return form return form

View File

@ -939,6 +939,20 @@ class APIClientForm(forms.ModelForm):
'apiclient_roles', 'apiclient_roles',
) )
def clean(self):
ou = self.cleaned_data['ou']
if ou:
unauthorized_roles = self.cleaned_data['apiclient_roles'].exclude(ou=ou)
if unauthorized_roles:
unauthorized_roles = ', '.join(unauthorized_roles.values_list('name', flat=True))
self.add_error(
'apiclient_roles',
_(
f'The following roles do not belong to organizational unit {ou.name}: {unauthorized_roles}.'
),
)
return super().clean()
class Meta: class Meta:
model = APIClient model = APIClient
fields = ( fields = (

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.26 on 2022-12-22 09:13
import django.db.models.deletion
from django.db import migrations, models
import authentic2.a2_rbac.utils
class Migration(migrations.Migration):
dependencies = [
('authentic2', '0044_apiclient_ou'),
]
operations = [
migrations.AlterField(
model_name='apiclient',
name='ou',
field=models.ForeignKey(
blank=True,
default=authentic2.a2_rbac.utils.get_default_ou_pk,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='a2_rbac.OrganizationalUnit',
verbose_name='organizational unit',
),
),
]

View File

@ -727,6 +727,8 @@ class APIClient(models.Model):
swappable=False, swappable=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
default=get_default_ou_pk, default=get_default_ou_pk,
null=True,
blank=True,
) )
class Meta: class Meta:

View File

@ -20,6 +20,7 @@ import pytest
from django.urls import reverse from django.urls import reverse
from authentic2.a2_rbac.models import Role from authentic2.a2_rbac.models import Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.models import APIClient from authentic2.models import APIClient
from .utils import login from .utils import login
@ -159,12 +160,13 @@ def test_list_show_objects_local_admin(admin_ou1, app, ou1, ou2):
def test_add(superuser, app): def test_add(superuser, app):
assert APIClient.objects.count() == 0 assert APIClient.objects.count() == 0
role_1 = Role.objects.create(name='role-1') role_1 = Role.objects.create(name='role-1', ou=get_default_ou())
role_2 = Role.objects.create(name='role-2') role_2 = Role.objects.create(name='role-2', ou=get_default_ou())
resp = login(app, superuser, 'a2-manager-api-client-add') resp = login(app, superuser, 'a2-manager-api-client-add')
form = resp.form form = resp.form
# password is prefilled # password is prefilled
assert form.get('password').value assert form.get('password').value
assert ('', False, '---------') in form['ou'].options
form.set('name', 'api-client-name') form.set('name', 'api-client-name')
form.set('description', 'api-client-description') form.set('description', 'api-client-description')
form.set('identifier', 'api-client-identifier') form.set('identifier', 'api-client-identifier')
@ -182,18 +184,20 @@ def test_add_local_admin(admin_ou1, app, ou1, ou2):
resp = login(app, admin_ou1, 'a2-manager-api-client-add') resp = login(app, admin_ou1, 'a2-manager-api-client-add')
form = resp.form form = resp.form
assert len(form['ou'].options) == 1 assert len(form['ou'].options) == 1
assert ('', False, '---------') not in form['ou'].options
assert form['ou'].options[0][2] == 'OU1' assert form['ou'].options[0][2] == 'OU1'
role = Role.objects.get(slug='_a2-manager-of-api-clients-%s' % ou2.slug) role = Role.objects.get(slug='_a2-manager-of-api-clients-%s' % ou2.slug)
admin_ou1.roles.add(role) admin_ou1.roles.add(role)
resp = app.get(reverse('a2-manager-api-client-add')) resp = app.get(reverse('a2-manager-api-client-add'))
assert len(resp.form['ou'].options) == 2 assert len(resp.form['ou'].options) == 2
assert ('', False, '---------') not in form['ou'].options
def test_add_description_non_mandatory(superuser, app): def test_add_description_non_mandatory(superuser, app):
assert APIClient.objects.count() == 0 assert APIClient.objects.count() == 0
role_1 = Role.objects.create(name='role-1') role_1 = Role.objects.create(name='role-1', ou=get_default_ou())
role_2 = Role.objects.create(name='role-2') role_2 = Role.objects.create(name='role-2', ou=get_default_ou())
resp = login(app, superuser, 'a2-manager-api-client-add') resp = login(app, superuser, 'a2-manager-api-client-add')
form = resp.form form = resp.form
form.set('name', 'api-client-name') form.set('name', 'api-client-name')
@ -238,23 +242,61 @@ def test_detail(superuser, app):
assert delete_button.text() == 'Delete' assert delete_button.text() == 'Delete'
def test_edit(superuser, app): def test_edit(superuser, app, ou1, ou2):
role_1 = Role.objects.create(name='role-1', ou=ou1)
role_2 = Role.objects.create(name='role-2', ou=ou2)
role_3 = Role.objects.create(name='role-3', ou=ou1)
api_client = APIClient.objects.create( api_client = APIClient.objects.create(
name='foo', description='foo-description', identifier='foo-identifier', password='foo-password' name='foo',
description='foo-description',
identifier='foo-identifier',
password='foo-password',
ou=ou1,
) )
assert APIClient.objects.count() == 1 assert APIClient.objects.count() == 1
resp = login(app, superuser, 'a2-manager-api-client-edit', kwargs={'pk': api_client.pk}) resp = login(app, superuser, 'a2-manager-api-client-edit', kwargs={'pk': api_client.pk})
form = resp.form form = resp.form
assert form.get('password').value == 'foo-password' assert form.get('password').value == 'foo-password'
assert ('', False, '---------') in form['ou'].options
resp.form.set('password', 'easy') resp.form.set('password', 'easy')
with pytest.raises(KeyError):
# forcing values not presented by the Select2ModelMultipleChoiceField,
# should not happen in UI
form['apiclient_roles'].force_value([role_1.id, role_2.id])
form.submit()
form['apiclient_roles'].force_value([role_1.id, role_3.id])
response = form.submit().follow() response = form.submit().follow()
assert urlparse(response.request.url).path == api_client.get_absolute_url() assert urlparse(response.request.url).path == api_client.get_absolute_url()
assert APIClient.objects.count() == 1 assert APIClient.objects.count() == 1
api_client = APIClient.objects.get(password='easy') api_client = APIClient.objects.get(password='easy')
assert api_client.identifier == 'foo-identifier' assert api_client.identifier == 'foo-identifier'
resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk}))
form = resp.form
form.set('ou', ou2.id)
response = form.submit()
errmsg = response.pyquery('div.error')[0].text
assert "do not belong to organizational unit OU2: role-1, role-3." in errmsg
response.form.set('ou', ou2.id)
response.form['apiclient_roles'].force_value([])
response.form.submit().follow()
api_client = APIClient.objects.get()
assert set(api_client.apiclient_roles.all()) == set()
assert api_client.ou == ou2
resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk}))
form = resp.form
form['apiclient_roles'].force_value([role_2.id])
response = form.submit().follow()
api_client = APIClient.objects.get()
assert api_client.ou == ou2
assert set(api_client.apiclient_roles.all()) == {role_2}
def test_edit_local_admin(admin_ou1, app, ou1, ou2): def test_edit_local_admin(admin_ou1, app, ou1, ou2):
role_1 = Role.objects.create(name='role-1', ou=ou1)
role_2 = Role.objects.create(name='role-2', ou=ou2)
role_3 = Role.objects.create(name='role-3', ou=ou1)
api_client_ou1 = APIClient.objects.create( api_client_ou1 = APIClient.objects.create(
name='foo', name='foo',
description='foo-description', description='foo-description',
@ -273,6 +315,13 @@ def test_edit_local_admin(admin_ou1, app, ou1, ou2):
form = resp.form form = resp.form
assert form.get('password').value == 'foo-password' assert form.get('password').value == 'foo-password'
resp.form.set('password', 'easy') resp.form.set('password', 'easy')
assert ('', False, '---------') not in form['ou'].options
with pytest.raises(KeyError):
# forcing values not presented by the Select2ModelMultipleChoiceField,
# should not happen in UI
form['apiclient_roles'].force_value([role_1.id, role_2.id])
form.submit()
form['apiclient_roles'].force_value([role_1.id, role_3.id])
response = form.submit().follow() response = form.submit().follow()
assert urlparse(response.request.url).path == api_client_ou1.get_absolute_url() assert urlparse(response.request.url).path == api_client_ou1.get_absolute_url()
api_client = APIClient.objects.get(password='easy') api_client = APIClient.objects.get(password='easy')
@ -282,6 +331,7 @@ def test_edit_local_admin(admin_ou1, app, ou1, ou2):
admin_ou1.roles.add(role) admin_ou1.roles.add(role)
resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client_ou2.pk})) resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client_ou2.pk}))
assert resp.form.get('password').value == 'bar-password' assert resp.form.get('password').value == 'bar-password'
assert ('', False, '---------') not in form['ou'].options
resp.form.set('ou', ou1.id) resp.form.set('ou', ou1.id)
resp.form.submit().follow() resp.form.submit().follow()
assert APIClient.objects.filter(ou=ou1).count() == 2 assert APIClient.objects.filter(ou=ou1).count() == 2