Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/grimoirelab/core/config/permission_groups.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
}
9 changes: 9 additions & 0 deletions src/grimoirelab/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/grimoirelab/core/datasources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions src/grimoirelab/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from django.conf import settings
from rest_framework.permissions import BasePermission
from rest_framework.response import Response


class IsAuthenticated(BasePermission):
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the implications of this? I'm afraid there could be other ways to call the API, etc and have a security hole because we don't remember this only works for the 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
63 changes: 63 additions & 0 deletions src/grimoirelab/core/runner/commands/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from __future__ import annotations

import getpass
import json
import os
import sys
import typing
Expand Down Expand Up @@ -61,6 +62,7 @@ def _setup():

_create_database()
_setup_database()
_setup_group_permissions()
_install_static_files()

click.secho("\nGrimoirelab configuration completed", fg='bright_cyan')
Expand Down Expand Up @@ -120,6 +122,44 @@ def _install_static_files():
click.echo()


def _setup_group_permissions():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I made this command before but, shouldn't this be a fixture? It can be like what we have in SortingHat for countries.

"""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,
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions src/grimoirelab/core/scheduler/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -76,6 +78,7 @@ def add_task(request):


@api_view(['POST'])
@check_permissions(['tasks.change_eventizertask'])
def reschedule_task(request):
"""Reschedule a Task

Expand All @@ -97,6 +100,7 @@ def reschedule_task(request):


@api_view(['POST'])
@check_permissions(['tasks.delete_eventizertask'])
def cancel_task(request):
"""Cancel a Task

Expand Down
1 change: 1 addition & 0 deletions src/grimoirelab/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 43 additions & 1 deletion tests/unit/datasources/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 = {
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can have something generic to try the permissions of specific actions.

"""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"
})