diff --git a/src/grimoirelab/core/config/permission_groups.json b/src/grimoirelab/core/config/permission_groups.json new file mode 100644 index 0000000..0d13a96 --- /dev/null +++ b/src/grimoirelab/core/config/permission_groups.json @@ -0,0 +1,31 @@ +{ + "groups": { + "admin": { + "datasources": { + "repository": ["add", "change", "delete", "view"] + }, + "tasks": { + "eventizerjob": ["add", "change", "delete", "view"], + "eventizertask": ["add", "change", "delete", "view"] + } + }, + "user": { + "datasources": { + "repository": ["add", "change", "delete", "view"] + }, + "tasks": { + "eventizerjob": ["add", "change", "delete", "view"], + "eventizertask": ["add", "change", "delete", "view"] + } + }, + "readonly": { + "datasources": { + "repository": ["view"] + }, + "tasks": { + "eventizerjob": ["view"], + "eventizertask": ["view"] + } + } + } +} diff --git a/src/grimoirelab/core/config/settings.py b/src/grimoirelab/core/config/settings.py index 046dc91..17c8cc9 100644 --- a/src/grimoirelab/core/config/settings.py +++ b/src/grimoirelab/core/config/settings.py @@ -274,6 +274,15 @@ ], } +# +# Path of the permission groups configuration file +# +# https://docs.djangoproject.com/en/4.2/topics/auth/default/#groups +# + +PERMISSION_GROUPS_LIST_PATH = os.environ.get('GRIMOIRELAB_PERMISSION_GROUPS_LIST_PATH', + os.path.join(BASE_DIR, 'config', 'permission_groups.json')) + # # GrimoireLab uses RQ to run background and async jobs. # You'll HAVE TO set the next parameters in order to run diff --git a/src/grimoirelab/core/datasources/views.py b/src/grimoirelab/core/datasources/views.py index 9a56cc9..5bf9db8 100644 --- a/src/grimoirelab/core/datasources/views.py +++ b/src/grimoirelab/core/datasources/views.py @@ -22,10 +22,12 @@ from django.db import IntegrityError, transaction from .models import Repository -from grimoirelab.core.scheduler.scheduler import schedule_task +from ..scheduler.scheduler import schedule_task +from ..permissions import check_permissions @api_view(['POST']) +@check_permissions(['datasources.add_repository']) def add_repository(request): """Create a Repository and start a Task to fetch items diff --git a/src/grimoirelab/core/permissions.py b/src/grimoirelab/core/permissions.py index c68c1cf..1ca795e 100644 --- a/src/grimoirelab/core/permissions.py +++ b/src/grimoirelab/core/permissions.py @@ -18,6 +18,7 @@ from django.conf import settings from rest_framework.permissions import BasePermission +from rest_framework.response import Response class IsAuthenticated(BasePermission): @@ -30,3 +31,19 @@ def has_permission(self, request, view): return True return bool(request.user and request.user.is_authenticated) + + +def check_permissions(permissions): + """ + Decorator to check if the user has the given permissions. + This only works for RestFramework views. + """ + def decorator(func): + def wrapper(request, *args, **kwargs): + if not settings.GRIMOIRELAB_AUTHENTICATION_REQUIRED: + return func(request, *args, **kwargs) + if not request.user or not request.user.has_perms(permissions): + return Response({'message': 'You do not have permission to perform this action.'}, status=403) + return func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/src/grimoirelab/core/runner/commands/admin.py b/src/grimoirelab/core/runner/commands/admin.py index aee180d..f12ca49 100644 --- a/src/grimoirelab/core/runner/commands/admin.py +++ b/src/grimoirelab/core/runner/commands/admin.py @@ -19,6 +19,7 @@ from __future__ import annotations import getpass +import json import os import sys import typing @@ -61,6 +62,7 @@ def _setup(): _create_database() _setup_database() + _setup_group_permissions() _install_static_files() click.secho("\nGrimoirelab configuration completed", fg='bright_cyan') @@ -120,6 +122,44 @@ def _install_static_files(): click.echo() +def _setup_group_permissions(): + """Create groups with the chosen permissions.""" + + from django.conf import settings + from django.contrib.auth.models import Group, Permission + from django.contrib.contenttypes.models import ContentType + + with open(settings.PERMISSION_GROUPS_LIST_PATH, 'r') as f: + groups = json.load(f).get('groups', []) + + for group_name, content_types in groups.items(): + new_group, created = Group.objects.get_or_create(name=group_name) + + for app_label, models in content_types.items(): + for model, permissions in models.items(): + try: + content_type = ContentType.objects.get( + app_label=app_label, + model=model + ) + for permission_name in permissions: + codename = f"{permission_name}_{model}" + if model == "custompermissions": + codename = permission_name + try: + permission = Permission.objects.get( + codename=codename, + content_type=content_type + ) + new_group.permissions.add(permission) + except Permission.DoesNotExist: + click.echo(f"Permission {permission_name} not found") + continue + except ContentType.DoesNotExist: + click.echo(f"ContentType {model} not found in {app_label}") + continue + + @admin.command() @click.option('--username', help="Specifies the login for the user.") @click.option('--is-admin', is_flag=True, default=False, @@ -186,6 +226,29 @@ def _validate_username(username): return '; '.join(e.messages) +@admin.command() +@click.argument('username') +@click.argument('permission_group') +def set_permissions(username, permission_group): + """Assign a user to a specific permission group""" + + from django.contrib.auth.models import Group + User = get_user_model() + + try: + group = Group.objects.get(name=permission_group) + user = User.objects.get(username=username) + except Group.DoesNotExist: + click.echo(f"Group '{permission_group}' not found") + sys.exit(1) + except User.DoesNotExist: + click.echo(f"User '{username}' not found") + sys.exit(1) + + user.groups.set([group.id]) + click.echo(f"User '{username}' assigned to group '{permission_group}'.") + + @admin.group() @click.pass_context def queues(ctx: Context): diff --git a/src/grimoirelab/core/scheduler/views.py b/src/grimoirelab/core/scheduler/views.py index 6321e33..2323845 100644 --- a/src/grimoirelab/core/scheduler/views.py +++ b/src/grimoirelab/core/scheduler/views.py @@ -25,9 +25,11 @@ schedule_task, reschedule_task as scheduler_reschedule_task ) +from ..permissions import check_permissions @api_view(['POST']) +@check_permissions(['tasks.add_eventizertask']) def add_task(request): """Create a Task to fetch items @@ -76,6 +78,7 @@ def add_task(request): @api_view(['POST']) +@check_permissions(['tasks.change_eventizertask']) def reschedule_task(request): """Reschedule a Task @@ -97,6 +100,7 @@ def reschedule_task(request): @api_view(['POST']) +@check_permissions(['tasks.delete_eventizertask']) def cancel_task(request): """Cancel a Task diff --git a/src/grimoirelab/core/views.py b/src/grimoirelab/core/views.py index 3e9a2b7..37bb964 100644 --- a/src/grimoirelab/core/views.py +++ b/src/grimoirelab/core/views.py @@ -46,5 +46,6 @@ def api_login(request): response = { 'user': username, 'isAdmin': user.is_superuser, + 'groups': [group['name'] for group in user.groups.values('name')] } return Response(response) diff --git a/tests/unit/datasources/test_views.py b/tests/unit/datasources/test_views.py index ad2e9f2..16e227d 100644 --- a/tests/unit/datasources/test_views.py +++ b/tests/unit/datasources/test_views.py @@ -21,6 +21,7 @@ from unittest.mock import patch from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission from django.test import TestCase, Client from django.urls import reverse @@ -33,7 +34,7 @@ class TestAddRepository(TestCase): def setUp(self): self.client = Client() - self.user = get_user_model().objects.create_user(username='testuser', password='testpassword') + self.user = get_user_model().objects.create_user(username='testuser', password='testpassword', is_superuser=True) self.client.login(username='testuser', password='testpassword') self.url = reverse('add_repository') self.valid_data = { @@ -132,3 +133,44 @@ def test_add_repository_authentication_required(self): self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), {"detail": "Authentication credentials were not provided."}) + + def test_add_repository_permission_denied(self): + """Test adding a repository with insufficient permissions.""" + + # Create a user without permissions + get_user_model().objects.create_user(username='nopermuser', password='nopermpassword') + self.client.login(username='nopermuser', password='nopermpassword') + + response = self.client.post( + self.url, + data=json.dumps(self.valid_data), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), {"message": "You do not have permission to perform this action."}) + + @patch('grimoirelab.core.datasources.views.schedule_task') + def test_add_repository_valid_permissions(self, mock_schedule_task): + """Test adding a repository with valid permissions.""" + + mock_schedule_task.return_value = self.task + + # Create a user with permissions + user = get_user_model().objects.create_user(username='user', password='password') + perm = Permission.objects.get(codename='add_repository') + user.user_permissions.add(perm) + self.client.login(username='user', password='password') + + response = self.client.post( + self.url, + data=json.dumps(self.valid_data), + content_type='application/json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + 'status': 'ok', + 'task_id': self.task.uuid, + 'message': f"Repository {self.valid_data['uri']} added correctly" + })