From b93534c13cac77fa09aa662d863a86be29d0db5f Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 3 Mar 2021 08:59:01 +0000 Subject: [PATCH 1/3] Allowing managers to create LDAP groups. --- jasmin_services/forms.py | 36 ++++ .../includes/service_tabs.html | 8 +- .../jasmin_services/service_group.html | 60 +++++++ jasmin_services/urls.py | 1 + jasmin_services/views.py | 155 +++++++++++++++++- 5 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 jasmin_services/templates/jasmin_services/service_group.html diff --git a/jasmin_services/forms.py b/jasmin_services/forms.py index 4ddecdc..897b6cf 100644 --- a/jasmin_services/forms.py +++ b/jasmin_services/forms.py @@ -65,6 +65,42 @@ def message_form_factory(sender, *roles): }) +def group_form_factory(*roles): + """ + Factory function that creates a group form for a set of roles. + + The set of users is those with a valid, active grant for one of the roles. + """ + # The possible users are those that have an active, non-revoked, non-expired + # grant for the USER role for the service + queryset = get_user_model().objects.distinct() \ + .filter( + grant__in = Grant.objects + .filter( + role__in = roles, + expires__gte = date.today(), + revoked = False + ) + .filter_active() + ).distinct() + return type(uuid.uuid4().hex, (forms.Form, ), { + 'name' : forms.CharField(label = 'Name', required = True, max_length = 15), + 'description' : forms.CharField(label = 'Description'), + 'users' : forms.ModelMultipleChoiceField( + queryset = queryset, + label = 'Initial users' + ), + }) + + +class GroupForm(forms.Form): + """ + Form for creating a key LDAP group for an object store. + """ + name = forms.CharField(label = 'Name', required = True, max_length = 15) + description = forms.CharField(label = 'Description') + + class DecisionForm(forms.Form): """ Form for making a decision on a request. diff --git a/jasmin_services/templates/jasmin_services/includes/service_tabs.html b/jasmin_services/templates/jasmin_services/includes/service_tabs.html index 5dfd2ee..26d4016 100644 --- a/jasmin_services/templates/jasmin_services/includes/service_tabs.html +++ b/jasmin_services/templates/jasmin_services/includes/service_tabs.html @@ -3,9 +3,10 @@ {% user_has_service_perm service request.user 'jasmin_services.view_users_role' as view_users %} {% user_has_service_perm service request.user 'jasmin_services.decide_request' as approver %} {% user_has_service_perm service request.user 'jasmin_services.send_message_role' as send_message %} +{% user_has_service_perm service request.user 'jasmin_services.create_group_role' as create_group %} {% with active=request.resolver_match.url_name %} - {% if view_users or approver or send_message %} + {% if view_users or approver or send_message or create_group %} {% endif %} {% endwith %} diff --git a/jasmin_services/templates/jasmin_services/service_group.html b/jasmin_services/templates/jasmin_services/service_group.html new file mode 100644 index 0000000..162217a --- /dev/null +++ b/jasmin_services/templates/jasmin_services/service_group.html @@ -0,0 +1,60 @@ +{% extends 'layout.html' %} +{% load staticfiles bootstrap3 %} + +{% block head_js_extra %}{{ block.super }} + +{% endblock %} + +{% block stylesheets_extra %}{{ block.super }} + +{% endblock %} + +{% block page_title %}Create a group{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content_header %}{{ block.super }} +
+
+ {% include "jasmin_services/includes/service_tabs.html" %} +
+
+{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} + + {% bootstrap_form form layout='horizontal' %} + +
+
+ +
+
+
+
+ +{% endblock %} diff --git a/jasmin_services/urls.py b/jasmin_services/urls.py index 989e555..6354242 100644 --- a/jasmin_services/urls.py +++ b/jasmin_services/urls.py @@ -26,6 +26,7 @@ url(r'^requests/$', views.service_requests, name = 'service_requests'), url(r'^users/$', views.service_users, name = 'service_users'), url(r'^message/$', views.service_message, name = 'service_message'), + url(r'^groups/$', views.service_groups, name = 'service_groups'), ])), url(r'^(?P[\w-]+)/(?P[\w-]+)/apply/(?P[\w-]+)/$', views.role_apply, diff --git a/jasmin_services/views.py b/jasmin_services/views.py index dfaf995..b0e810a 100644 --- a/jasmin_services/views.py +++ b/jasmin_services/views.py @@ -25,12 +25,15 @@ from django.template.loader import render_to_string from django.db import transaction from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission from django.contrib.auth.decorators import user_passes_test +from django.contrib.contenttypes.models import ContentType from django.contrib.auth.decorators import login_required -from .models import Grant, Request, RequestState, Category, Service, Role -from .forms import DecisionForm, message_form_factory +from jasmin_metadata.models import Metadatum, Form +from .models import Grant, Request, RequestState, Category, Service, Role, Group, RoleObjectPermission, LdapGroupBehaviour, CedaLdapGroup +from .forms import DecisionForm, message_form_factory, group_form_factory _log = logging.getLogger(__name__) @@ -801,6 +804,154 @@ def service_message(request, service): 'form': form, }) +# from . import models as service_models +# CedaLdapGroup = getattr(models, 'CedaLdapGroup') +@require_http_methods(['GET', 'POST']) +@login_required +@with_service +def service_groups(request, service): + """ + Handler for ``///groups/``. + + Responds to GET and POST. The user must have the ``create_groups_role`` + permission for at least one role for the service. + + Allows a user with suitable permissions to create an LDAP group which other + users of the service can apply for, depending which permissions they have + been granted. + """ + # Get the roles for which the user is allowed to create groups + # We allow the permission to be allocated for all services, per-service or per-role + permission = 'jasmin_services.create_group_role' + if request.user.has_perm(permission) or \ + request.user.has_perm(permission, service): + user_roles = list(service.roles.all()) + else: + user_roles = [ + role + for role in service.roles.all() + if request.user.has_perm(permission, role) + ] + # If the user has no permissions, send them back to the service details + # Note that we don't show this message if the user has been granted the + # permission for the service but there are no roles - in that case we + # just show nothing + if not user_roles: + messages.error(request, 'Insufficient permissions') + return redirect_to_service(service) + GroupForm = group_form_factory(*user_roles) + if request.method == 'POST': + form = GroupForm(request.POST) + if form.is_valid(): + try: + name = form.cleaned_data['name'] + description = form.cleaned_data['description'] + member_uids = [u.account.uid for u in form.cleaned_data['users']] + LDAP_group_name = (service.name + '_' + name).replace('-', '_') + default_form = Form.objects.get(pk = settings.JASMIN_SERVICES['DEFAULT_METADATA_FORM']) + + # Check if there is already a role with this name + try: + Role.objects.get( + service = service, + name = name, + ) + raise Exception('Already exists') + except Role.DoesNotExist: + role = Role( + service = service, + name = name, + description = description, + metadata_form = default_form, + hidden = False, + ) + + # Check if there is already a ceda ldap group with this name + try: + CedaLdapGroup.objects.get(name = LDAP_group_name) + raise Exception('Already exists') + except CedaLdapGroup.DoesNotExist: + group = CedaLdapGroup( + name = LDAP_group_name, + description = description, + member_uids = member_uids, + ) + + # Save both after check so not to create only one + role.save() + group.save() + + # Create the LDAP group behaviour + ldap_group_behaviour = LdapGroupBehaviour.objects.create( + ldap_model = CedaLdapGroup, + group_name = LDAP_group_name, + ) + role.behaviours.add(ldap_group_behaviour) + + # Create relevant role_object_permissions to the manager and deputy + permissions = [ + Permission.objects.get( + content_type = ContentType.objects.get_for_model(Role), + codename = 'view_users_role', + ), + Permission.objects.get( + content_type = ContentType.objects.get_for_model(Role), + codename = 'send_message_role', + ), + Permission.objects.get( + content_type = ContentType.objects.get_for_model(Request), + codename = 'decide_request', + ), + ] + + management_roles = [ + Role.objects.get( + service = service, + name = 'MANAGER', + ), + Role.objects.get( + service = service, + name = 'DEPUTY', + ), + ] + + for management_role in management_roles: + RoleObjectPermission.objects.bulk_create([ + RoleObjectPermission( + role = management_role, + permission = permission, + content_type = ContentType.objects.get_for_model(role), + object_pk = role.pk + ) + for permission in permissions + ]) + + messages.success(request, 'Group created') + return redirect_to_service(service) + except Exception as e: + print(e) + if str(e) == 'Already exists': + messages.error(request, 'Group with this name already exists') + else: + messages.error(request, 'Error creating group') + + else: + messages.error(request, 'Error with one or more fields') + else: + form = GroupForm() + templates = [ + 'jasmin_services/{}/{}/service_group.html'.format( + service.category.name, + service.name + ), + 'jasmin_services/{}/service_group.html'.format(service.category.name), + 'jasmin_services/service_group.html', + ] + return render(request, templates, { + 'service': service, + 'form': form, + }) + @require_safe def reverse_dns_check(request): From d0c2a82b630afc661c131981bdbe82f9e6487bc8 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 3 Mar 2021 09:51:17 +0000 Subject: [PATCH 2/3] Creating grants and fixing no attribute error. --- jasmin_services/forms.py | 1 + jasmin_services/views.py | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/jasmin_services/forms.py b/jasmin_services/forms.py index 897b6cf..a24c609 100644 --- a/jasmin_services/forms.py +++ b/jasmin_services/forms.py @@ -87,6 +87,7 @@ def group_form_factory(*roles): 'name' : forms.CharField(label = 'Name', required = True, max_length = 15), 'description' : forms.CharField(label = 'Description'), 'users' : forms.ModelMultipleChoiceField( + required = False, queryset = queryset, label = 'Initial users' ), diff --git a/jasmin_services/views.py b/jasmin_services/views.py index b0e810a..8400a97 100644 --- a/jasmin_services/views.py +++ b/jasmin_services/views.py @@ -846,7 +846,8 @@ def service_groups(request, service): try: name = form.cleaned_data['name'] description = form.cleaned_data['description'] - member_uids = [u.account.uid for u in form.cleaned_data['users']] + users = form.cleaned_data['users'] + member_uids = [user.account.uid for user in users] LDAP_group_name = (service.name + '_' + name).replace('-', '_') default_form = Form.objects.get(pk = settings.JASMIN_SERVICES['DEFAULT_METADATA_FORM']) @@ -876,18 +877,28 @@ def service_groups(request, service): description = description, member_uids = member_uids, ) - - # Save both after check so not to create only one - role.save() - group.save() - + # Create the LDAP group behaviour ldap_group_behaviour = LdapGroupBehaviour.objects.create( - ldap_model = CedaLdapGroup, + ldap_model = group, group_name = LDAP_group_name, ) + + # Save both after check so not to create only one + role.save() + group.save() role.behaviours.add(ldap_group_behaviour) + # Create a grant for each initial user + for user in users: + Grant.objects.create( + role = role, + user = user, + granted_by = request.user.username, + expires = date.today() + relativedelta(years = 1) + ) + + # Create relevant role_object_permissions to the manager and deputy permissions = [ Permission.objects.get( From e009870d1e97856391fbd74cf934cabb5d6f7fa3 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Mon, 8 Mar 2021 11:23:18 +0000 Subject: [PATCH 3/3] First review changes. --- jasmin_services/forms.py | 30 +++----- .../migrations/0011_auto_20210308_0936.py | 19 +++++ jasmin_services/models/base.py | 3 + jasmin_services/views.py | 72 +++++-------------- 4 files changed, 51 insertions(+), 73 deletions(-) create mode 100644 jasmin_services/migrations/0011_auto_20210308_0936.py diff --git a/jasmin_services/forms.py b/jasmin_services/forms.py index a24c609..39f2859 100644 --- a/jasmin_services/forms.py +++ b/jasmin_services/forms.py @@ -22,7 +22,7 @@ from markdown_deux.templatetags.markdown_deux_tags import markdown_allowed -from .models import Grant, RequestState, LdapGroupBehaviour +from .models import Grant, RequestState, LdapGroupBehaviour, Role def message_form_factory(sender, *roles): @@ -65,31 +65,23 @@ def message_form_factory(sender, *roles): }) -def group_form_factory(*roles): +def group_form_factory(service): """ Factory function that creates a group form for a set of roles. - - The set of users is those with a valid, active grant for one of the roles. """ - # The possible users are those that have an active, non-revoked, non-expired - # grant for the USER role for the service - queryset = get_user_model().objects.distinct() \ - .filter( - grant__in = Grant.objects - .filter( - role__in = roles, - expires__gte = date.today(), - revoked = False - ) - .filter_active() - ).distinct() + ROLE_CHOICES = [(role, role.name) for role in service.roles.all()] + return type(uuid.uuid4().hex, (forms.Form, ), { 'name' : forms.CharField(label = 'Name', required = True, max_length = 15), 'description' : forms.CharField(label = 'Description'), - 'users' : forms.ModelMultipleChoiceField( + 'approver_roles' : forms.MultipleChoiceField( required = False, - queryset = queryset, - label = 'Initial users' + choices = ROLE_CHOICES, + label = 'Approver roles', + help_text = mark_safe( + 'The selected roles will be able to approve request for ' + 'the created role.' + ) ), }) diff --git a/jasmin_services/migrations/0011_auto_20210308_0936.py b/jasmin_services/migrations/0011_auto_20210308_0936.py new file mode 100644 index 0000000..82d4b9d --- /dev/null +++ b/jasmin_services/migrations/0011_auto_20210308_0936.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.9 on 2021-03-08 09:36 + +import django.core.validators +from django.db import migrations +import jasmin_ldap_django.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jasmin_services', '0010_auto_20190916_1328'), + ] + + operations = [ + migrations.AlterModelOptions( + name='service', + options={'ordering': ('category__position', 'category__long_name', 'position', 'name'), 'permissions': (('create_group_role', 'Can great a new group'),)}, + ), + ] diff --git a/jasmin_services/models/base.py b/jasmin_services/models/base.py index ac59a5c..cb6c767 100644 --- a/jasmin_services/models/base.py +++ b/jasmin_services/models/base.py @@ -64,6 +64,9 @@ class Meta: 'position', 'name' ) + permissions = ( + ('create_group_role', 'Can great a new group'), + ) #: The category that the service belongs to category = models.ForeignKey(Category, models.CASCADE, diff --git a/jasmin_services/views.py b/jasmin_services/views.py index 8400a97..237a352 100644 --- a/jasmin_services/views.py +++ b/jasmin_services/views.py @@ -814,44 +814,30 @@ def service_groups(request, service): Handler for ``///groups/``. Responds to GET and POST. The user must have the ``create_groups_role`` - permission for at least one role for the service. + permission for the service. Allows a user with suitable permissions to create an LDAP group which other - users of the service can apply for, depending which permissions they have - been granted. + users of the service can apply for. """ - # Get the roles for which the user is allowed to create groups - # We allow the permission to be allocated for all services, per-service or per-role + # The current user must have permission to create a group. permission = 'jasmin_services.create_group_role' - if request.user.has_perm(permission) or \ - request.user.has_perm(permission, service): - user_roles = list(service.roles.all()) - else: - user_roles = [ - role - for role in service.roles.all() - if request.user.has_perm(permission, role) - ] - # If the user has no permissions, send them back to the service details - # Note that we don't show this message if the user has been granted the - # permission for the service but there are no roles - in that case we - # just show nothing - if not user_roles: - messages.error(request, 'Insufficient permissions') - return redirect_to_service(service) - GroupForm = group_form_factory(*user_roles) + if not request.user.has_perm(permission) and \ + not request.user.has_perm(permission, service): + messages.error(request, 'Insufficient permissions') + return redirect_to_service(service) + + GroupForm = group_form_factory(service) if request.method == 'POST': form = GroupForm(request.POST) if form.is_valid(): try: name = form.cleaned_data['name'] description = form.cleaned_data['description'] - users = form.cleaned_data['users'] - member_uids = [user.account.uid for user in users] + approver_roles = form.cleaned_data['approver_roles'] LDAP_group_name = (service.name + '_' + name).replace('-', '_') default_form = Form.objects.get(pk = settings.JASMIN_SERVICES['DEFAULT_METADATA_FORM']) - # Check if there is already a role with this name + # Check if there is already a role with this name. try: Role.objects.get( service = service, @@ -867,7 +853,7 @@ def service_groups(request, service): hidden = False, ) - # Check if there is already a ceda ldap group with this name + # Check if there is already a ceda ldap group with this name. try: CedaLdapGroup.objects.get(name = LDAP_group_name) raise Exception('Already exists') @@ -875,31 +861,20 @@ def service_groups(request, service): group = CedaLdapGroup( name = LDAP_group_name, description = description, - member_uids = member_uids, ) - # Create the LDAP group behaviour + # Create the LDAP group behaviour. ldap_group_behaviour = LdapGroupBehaviour.objects.create( ldap_model = group, group_name = LDAP_group_name, ) - # Save both after check so not to create only one - role.save() + # Save both after check, as not to create only one. group.save() + role.save() role.behaviours.add(ldap_group_behaviour) - - # Create a grant for each initial user - for user in users: - Grant.objects.create( - role = role, - user = user, - granted_by = request.user.username, - expires = date.today() + relativedelta(years = 1) - ) - - # Create relevant role_object_permissions to the manager and deputy + # Create relevant role_object_permissions for the approver roles. permissions = [ Permission.objects.get( content_type = ContentType.objects.get_for_model(Role), @@ -915,21 +890,10 @@ def service_groups(request, service): ), ] - management_roles = [ - Role.objects.get( - service = service, - name = 'MANAGER', - ), - Role.objects.get( - service = service, - name = 'DEPUTY', - ), - ] - - for management_role in management_roles: + for approver_role in approver_roles: RoleObjectPermission.objects.bulk_create([ RoleObjectPermission( - role = management_role, + role = approver_role, permission = permission, content_type = ContentType.objects.get_for_model(role), object_pk = role.pk