manager: start api client interface (#68404)

This commit is contained in:
Emmanuel Cazenave 2022-08-25 18:01:02 +02:00
parent 2be6ad6859
commit e6ff6db061
11 changed files with 322 additions and 5 deletions

View File

@ -35,7 +35,7 @@ from authentic2.a2_rbac.utils import generate_slug, get_default_ou
from authentic2.forms.fields import CheckPasswordField, NewPasswordField, ValidatedEmailField
from authentic2.forms.mixins import SlugMixin
from authentic2.forms.profile import BaseUserForm
from authentic2.models import PasswordReset
from authentic2.models import APIClient, PasswordReset
from authentic2.passwords import generate_password
from authentic2.utils.misc import (
import_module_or_class,
@ -49,6 +49,7 @@ from django_rbac.models import Operation
from . import app_settings, fields, utils
User = get_user_model()
ChooseRolesField = fields.ChooseRolesField
logger = logging.getLogger(__name__)
@ -897,3 +898,26 @@ class ChooseUserOrRoleForm(FormWithRequest, forms.Form):
perm = '%s.search_%s' % (User._meta.app_label, User._meta.model_name)
return user.filter_by_perm(perm, qs)
class APIClientForm(forms.ModelForm):
field_order = (
'name',
'description',
'identifier',
'password',
'restrict_to_anonymised_data',
'apiclient_roles',
)
class Meta:
model = APIClient
fields = (
'name',
'description',
'identifier',
'password',
'restrict_to_anonymised_data',
'apiclient_roles',
)
field_classes = {'apiclient_roles': ChooseRolesField}

View File

@ -0,0 +1,11 @@
{% extends "authentic2/manager/base.html" %}
{% load i18n %}
{% block page-title %}{{ block.super }} - {% if object %}{{ object }}{% else %}{% trans "API Clients" %}{% endif %}{% endblock %}
{% block title %}{{ block.super }} - {% trans "API Clients" %}{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'a2-manager-api-clients' %}">{% trans "API Clients" %}</a>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "authentic2/manager/api_client_common.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'a2-manager-api-clients' %}">{% trans "API clients" %}</a>
<a href="#"></a>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>{% blocktrans %}Do you want to delete "{{ object }}" ?{% endblocktrans %}</p>
<div class="buttons">
<button class="delete-button">{% trans "Delete" %}</button>
<a class="cancel" href="{% url 'a2-manager-api-client-detail' pk=object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "authentic2/manager/api_client_common.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
{{ block.super }}
<a href="#"></a>
{% endblock %}
{% block appbar %}
{{ block.super }}
<span class="actions">
<a href="{% url 'a2-manager-api-client-delete' pk=api_client.pk %}" rel="popup">{% trans "Delete" %}</a>
<a href="{% url 'a2-manager-api-client-edit' pk=api_client.pk %}">{% trans "Edit" %}</a>
</span>
{% endblock %}
{% block main %}
{% if api_client.description %}
<div class="bo-block">{{api_client.description}}</div>
{% endif %}
<div class="bo-block">
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "identifier" %}&nbsp;: {{api_client.identifier}}</li>
<li>{% trans "password" %}&nbsp;: {{api_client.password}}</li>
{% if api_client.restrict_to_anonymised_data %}<li>{% trans "Restricted to anonymised data" %}</li>{% endif %}
{% if api_client.apiclient_roles.count %}
<li>{% trans "Roles:" %}
<ul>
{% for role in api_client.apiclient_roles.all %}<li>{{ role.name }}</li>{% endfor %}
</ul>
</li>
{% endif %}
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "authentic2/manager/api_client_common.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
{{ block.super }}
<a href="#"></a>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button>{% trans "Submit" %}</button>
<a class="cancel" href="{{cancel_url}}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "authentic2/manager/api_client_common.html" %}
{% load i18n gadjo %}
{% block appbar %}
{{ block.super }}
<span class="actions">
<a href="{% url 'a2-manager-api-client-add' %}">{% trans "Add new API client" %}</a>
</span>
{% endblock %}
{% block main %}
{% if object_list %}
<ul class="objects-list single-links">
{% for api_client in object_list %}
<li>
<a href="{% url 'a2-manager-api-client-detail' pk=api_client.pk %}">{{api_client.name}} ({{api_client.identifier}})</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">{% trans "There are no API client defined." %}</div>
{% endif %}
{% endblock %}

View File

@ -41,6 +41,7 @@
{% if display_tech_info %}
<a id="tech-info" class="button button-paragraph" href="{% url 'a2-manager-tech-info' %}">{% trans 'Directory servers' %}</a>
{% endif %}
<a id="api-clients" class="button button-paragraph" href="{% url 'a2-manager-api-clients' %}">{% trans 'API Clients' %}</a>
</div>
</div>

View File

@ -191,6 +191,13 @@ urlpatterns = required(
url(r'^site-import/$', views.site_import, name='a2-manager-site-import'),
# technical information including ldap config
url(r'^tech-info/$', views.tech_info, name='a2-manager-tech-info'),
url(r'^api-clients/$', views.api_clients, name='a2-manager-api-clients'),
url(r'^api-clients/add/$', views.api_client_add, name='a2-manager-api-client-add'),
url(r'^api-clients/(?P<pk>\d+)/$', views.api_client_detail, name='a2-manager-api-client-detail'),
url(r'^api-clients/(?P<pk>\d+)/edit/$', views.api_client_edit, name='a2-manager-api-client-edit'),
url(
r'^api-clients/(?P<pk>\d+)/delete/$', views.api_client_delete, name='a2-manager-api-client-delete'
),
],
)

View File

@ -35,6 +35,7 @@ from django.views.generic import CreateView, DeleteView, DetailView, FormView, T
from django.views.generic.base import ContextMixin
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin
from django.views.generic.list import ListView
from django_select2.views import AutoResponseView
from django_tables2 import SingleTableMixin, SingleTableView
from gadjo.templatetags.gadjo import xstatic
@ -45,6 +46,7 @@ from authentic2.backends import ldap_backend
from authentic2.data_transfer import ImportContext, export_site, import_site
from authentic2.decorators import json as json_view
from authentic2.forms.profile import modelform_factory
from authentic2.models import APIClient
from authentic2.utils import crypto
from authentic2.utils.misc import batch_queryset, redirect
@ -843,3 +845,75 @@ class SearchOUMixin:
def get_context_data(self, **kwargs):
return super().get_context_data(ou=self.ou, **kwargs)
class APIClientsMixin(MediaMixin, TitleMixin):
model = APIClient
def get_queryset(self):
return self.model.objects.all()
class APIClientsView(APIClientsMixin, ListView):
template_name = 'authentic2/manager/api_clients.html'
title = _('API clients')
api_clients = APIClientsView.as_view()
class APIClientDetailView(APIClientsMixin, DetailView):
template_name = 'authentic2/manager/api_client_detail.html'
@property
def title(self):
return str(self.object)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['api_client'] = self.object
return context
api_client_detail = APIClientDetailView.as_view()
class APIClientAddView(APIClientsMixin, CreateView):
template_name = 'authentic2/manager/api_client_form.html'
title = _('New API client')
form_class = forms.APIClientForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse('a2-manager-api-clients')
return context
def get_success_url(self):
return reverse('a2-manager-api-client-detail', kwargs={'pk': self.object.pk})
api_client_add = APIClientAddView.as_view()
class APIClientEditView(APIClientsMixin, UpdateView):
template_name = 'authentic2/manager/api_client_form.html'
title = _('Edit API client')
form_class = forms.APIClientForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse('a2-manager-api-client-detail', kwargs={'pk': self.object.pk})
return context
api_client_edit = APIClientEditView.as_view()
class APIClientDeleteView(APIClientsMixin, DeleteView):
template_name = 'authentic2/manager/api_client_delete.html'
title = _('Delete API client')
success_url = reverse_lazy('a2-manager-api-clients')
api_client_delete = APIClientDeleteView.as_view()

View File

@ -633,6 +633,11 @@ class APIClient(models.Model):
verbose_name = _('APIClient')
verbose_name_plural = _('APIClient')
def __str__(self):
if self.name:
return '%s - %s' % (self._meta.verbose_name, self.name)
return str(self._meta.verbose_name)
@property
def is_active(self):
return True
@ -704,3 +709,6 @@ class APIClient(models.Model):
return functools.reduce(operator.__or__, results)
else:
return qs
def get_absolute_url(self):
return reverse('a2-manager-api-client-detail', kwargs={'pk': self.pk})

View File

@ -33,7 +33,7 @@ from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.models import Permission, Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.journal.models import Event
from authentic2.models import Service
from authentic2.models import APIClient, Service
from authentic2.validators import EmailValidator
from authentic2_idp_oidc import app_settings as oidc_app_settings
from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
@ -61,8 +61,8 @@ def test_manager_login(superuser_or_admin, app):
path = reverse('a2-manager-%s' % section)
assert manager_home_page.pyquery.remove_namespaces()('a.button[href=\'%s\']' % path)
conf_entries = ['authn', 'journal']
assert len(manager_home_page.pyquery('div.a2-manager-id-tools_content').children()) == 2
conf_entries = ['authn', 'journal', 'api-clients']
assert len(manager_home_page.pyquery('div.a2-manager-id-tools_content').children()) == 3
for entry in conf_entries:
assert manager_home_page.pyquery(f'a#{entry}')
@ -71,7 +71,7 @@ def test_manager_login(superuser_or_admin, app):
with mock.patch('authentic2.backends.ldap_backend.LDAPBackend.get_config', return_value=mocked_config):
# new tech info link appears
manager_home_page = app.get(reverse('a2-manager-homepage'))
assert len(manager_home_page.pyquery('div.a2-manager-id-tools_content').children()) == 3
assert len(manager_home_page.pyquery('div.a2-manager-id-tools_content').children()) == 4
assert manager_home_page.pyquery('a#tech-info')
@ -1339,3 +1339,99 @@ def test_manager_menu_json(app, admin):
response = login(app, admin)
response = app.get('/manage/menu.json')
assert response.json == expected
def test_manager_api_client_list_empty(superuser_or_admin, app):
resp = login(app, superuser_or_admin, 'a2-manager-api-clients')
assert 'There are no API client defined.' in resp.text
def test_manager_api_client_list_add_button(superuser_or_admin, app):
resp = login(app, superuser_or_admin, 'a2-manager-api-clients')
anchor = resp.pyquery('span.actions a[href="%s"]' % reverse('a2-manager-api-client-add'))
assert anchor.text() == 'Add new API client'
def test_manager_api_client_list_show_objects(superuser_or_admin, app):
api_client = APIClient.objects.create(
name='foo', description='foo-description', identifier='foo-description', password='foo-password'
)
url = '/manage/api-clients/%s/' % api_client.pk
resp = login(app, superuser_or_admin, 'a2-manager-api-clients')
anchor = resp.pyquery('div.content ul.objects-list a[href="%s"]' % url)
assert anchor.text() == 'foo (foo-description)'
def test_manager_api_client_add(superuser_or_admin, app):
assert APIClient.objects.count() == 0
role_1 = Role.objects.create(name='role-1')
role_2 = Role.objects.create(name='role-2')
resp = login(app, superuser_or_admin, 'a2-manager-api-client-add')
form = resp.form
form.set('name', 'api-client-name')
form.set('description', 'api-client-description')
form.set('identifier', 'api-client-identifier')
form.set('password', 'api-client-password')
form['apiclient_roles'].force_value([role_1.id, role_2.id])
response = form.submit().follow()
assert APIClient.objects.count() == 1
api_client = APIClient.objects.get(name='api-client-name')
assert set(api_client.apiclient_roles.all()) == {role_1, role_2}
assert urlparse(response.request.url).path == api_client.get_absolute_url()
def test_manager_api_client_detail(superuser_or_admin, app):
role_1 = Role.objects.create(name='role-1')
role_2 = Role.objects.create(name='role-2')
api_client = APIClient.objects.create(
name='foo',
description='foo-description',
identifier='foo-identifier',
password='foo-password',
restrict_to_anonymised_data=True,
)
api_client.apiclient_roles.add(role_1, role_2)
resp = login(app, superuser_or_admin, api_client.get_absolute_url())
assert 'identifier&nbsp;: foo-identifier' in resp.text
assert 'password&nbsp;: foo-password' in resp.text
assert 'foo-description' in resp.text
assert 'Restricted to anonymised data' in resp.text
assert 'role-1' in resp.text
assert 'role-2' in resp.text
edit_button = resp.pyquery(
'span.actions a[href="%s"]' % reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk})
)
assert edit_button
assert edit_button.text() == 'Edit'
delete_button = resp.pyquery(
'span.actions a[href="%s"]' % reverse('a2-manager-api-client-delete', kwargs={'pk': api_client.pk})
)
assert delete_button
assert delete_button.text() == 'Delete'
def test_manager_api_client_edit(superuser_or_admin, app):
api_client = APIClient.objects.create(
name='foo', description='foo-description', identifier='foo-identifier', password='foo-password'
)
assert APIClient.objects.count() == 1
resp = login(app, superuser_or_admin, 'a2-manager-api-client-edit', kwargs={'pk': api_client.pk})
form = resp.form
resp.form.set('password', 'easy')
response = form.submit().follow()
assert urlparse(response.request.url).path == api_client.get_absolute_url()
assert APIClient.objects.count() == 1
api_client = APIClient.objects.get(password='easy')
assert api_client.identifier == 'foo-identifier'
def test_manager_api_client_delete(superuser_or_admin, app):
api_client = APIClient.objects.create(
name='foo', description='foo-description', identifier='foo-identifier', password='foo-password'
)
assert APIClient.objects.count() == 1
resp = login(app, superuser_or_admin, 'a2-manager-api-client-delete', kwargs={'pk': api_client.pk})
response = resp.form.submit().follow()
assert urlparse(response.request.url).path == reverse('a2-manager-api-clients')
assert APIClient.objects.count() == 0