diff --git a/build/views.py b/build/views.py index dd5faf2..68b509c 100644 --- a/build/views.py +++ b/build/views.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2020 The Sipwise Team - http://sipwise.com +# Copyright (C) 2017-2022 The Sipwise Team - http://sipwise.com # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free @@ -16,7 +16,7 @@ import django_filters from django.http import JsonResponse from django.shortcuts import get_object_or_404 from rest_framework import generics -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import DjangoModelPermissions from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_api_key.permissions import HasAPIKey @@ -37,14 +37,14 @@ class BuildReleaseFilter(django_filters.FilterSet): class BuildReleaseList(generics.ListCreateAPIView): - permission_classes = [HasAPIKey | IsAuthenticated] + permission_classes = [HasAPIKey | DjangoModelPermissions] queryset = models.BuildRelease.objects.all().order_by("id") serializer_class = serializers.BuildReleaseSerializer filter_class = BuildReleaseFilter class BuildReleaseDetail(generics.RetrieveDestroyAPIView): - permission_classes = [HasAPIKey | IsAuthenticated] + permission_classes = [HasAPIKey | DjangoModelPermissions] queryset = models.BuildRelease.objects.all().order_by("id") serializer_class = serializers.BuildReleaseSerializer @@ -69,7 +69,7 @@ class BuildReleaseDetail(generics.RetrieveDestroyAPIView): class BuildProject(APIView): - permission_classes = [HasAPIKey | IsAuthenticated] + permission_classes = [HasAPIKey | DjangoModelPermissions] def post(self, request, release_uuid, project): br = get_object_or_404(models.BuildRelease, uuid=release_uuid) diff --git a/panel/templates/panel/base_project.html b/panel/templates/panel/base_project.html index 4a6e26e..8c09e18 100644 --- a/panel/templates/panel/base_project.html +++ b/panel/templates/panel/base_project.html @@ -3,10 +3,12 @@
+ {% if perms.build.can_trigger %}
{% csrf_token %}
+ {% endif %} latest
diff --git a/panel/templates/panel/release_info.html b/panel/templates/panel/release_info.html index fb7d53c..b40a61d 100644 --- a/panel/templates/panel/release_info.html +++ b/panel/templates/panel/release_info.html @@ -16,11 +16,12 @@ {{ build_release.branch }} {{ build_release.start_date }} + {% if perms.build.can_trigger %} - + {% endif %} diff --git a/release_dashboard/templates/release_dashboard/build_release.html b/release_dashboard/templates/release_dashboard/build_release.html index 9719fe5..3cd3fd5 100644 --- a/release_dashboard/templates/release_dashboard/build_release.html +++ b/release_dashboard/templates/release_dashboard/build_release.html @@ -7,7 +7,7 @@ {% endblock %} {% block content %}
-
+

{{ config.release }}

@@ -27,7 +27,7 @@
{% csrf_token %} + {% if not done or not perms.build.can_trigger %}disabled="disabled"{% endif %}>Build
@@ -47,10 +47,11 @@ {{ br.last_update }} + {% if perms.build.can_trigger %}onclick="click_refresh_projects(event, '{{ br.id }}')"{% endif %} + class="btn btn-primary" {% if not perms.build.can_trigger %}disabled="disabled"{% endif %}>Refresh projects + {% if perms.build.can_trigger %}onclick="click_delete(event, '{{ br.id }}')"{% endif %} + class="btn btn-danger" {% if not perms.build.can_trigger %}disabled="disabled"{% endif %}>Delete {% endfor %} @@ -64,7 +65,8 @@

Projects to build

diff --git a/release_dashboard/test/test_views.py b/release_dashboard/test/test_views.py index 080510f..1931138 100644 --- a/release_dashboard/test/test_views.py +++ b/release_dashboard/test/test_views.py @@ -14,7 +14,9 @@ # with this prograproj. If not, see . from unittest.mock import patch +from django.contrib.auth.models import Permission from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.test import TestCase from django.urls import reverse @@ -23,13 +25,26 @@ from build.models import BuildRelease from repoapi.test.base import BaseTest +def add_perm(user, model, codename): + ct = ContentType.objects.get_for_model(model) + perm = Permission.objects.get(content_type=ct, codename=codename) + user.user_permissions.add(perm) + + class TestHotfix(TestCase): def test_no_login(self): res = self.client.get(reverse("release_dashboard:hotfix")) self.assertNotEqual(res.status_code, 200) + def test_login_no_perm(self): + user = User.objects.create_user(username="test") + self.client.force_login(user) + res = self.client.get(reverse("release_dashboard:hotfix")) + self.assertEqual(res.status_code, 403) + def test_login_ok(self): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) res = self.client.get(reverse("release_dashboard:hotfix")) self.assertEqual(res.status_code, 200) @@ -39,6 +54,7 @@ class TestHotfix(TestCase): @patch("release_dashboard.views.get_branches") def test_natural_sort(self, gb, gt): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) gt.return_value = [] gb.return_value = [ @@ -77,8 +93,19 @@ class TestHotfixRelease(TestCase): ) self.assertNotEqual(res.status_code, 200) + def test_login_no_perm(self): + user = User.objects.create_user(username="test") + self.client.force_login(user) + res = self.client.get( + reverse( + "release_dashboard:hotfix_release", args=["release-mr7.5.2"] + ) + ) + self.assertEqual(res.status_code, 403) + def test_login_ok(self): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) res = self.client.get( reverse( @@ -89,6 +116,7 @@ class TestHotfixRelease(TestCase): def test_no_mrXXX(self): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) res = self.client.get( reverse("release_dashboard:hotfix_release", args=["release-mr7.5"]) @@ -105,6 +133,7 @@ class TestHotfixRelease(TestCase): def test_project_ok(self): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) res = self.client.post( reverse( @@ -118,6 +147,7 @@ class TestHotfixRelease(TestCase): def test_project_wrong(self): user = User.objects.create_user(username="test") + add_perm(user, BuildRelease, "can_trigger_hotfix") self.client.force_login(user) res = self.client.post( reverse( diff --git a/release_dashboard/views/build.py b/release_dashboard/views/build.py index 31c80aa..5660029 100644 --- a/release_dashboard/views/build.py +++ b/release_dashboard/views/build.py @@ -17,6 +17,8 @@ import uuid import structlog from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import permission_required +from django.core.exceptions import PermissionDenied from django.http import HttpResponseNotFound from django.http import HttpResponseRedirect from django.http import JsonResponse @@ -52,6 +54,8 @@ def index(request): def build_release(request, release): release_config = ReleaseConfig(release) if request.method == "POST": + if not request.user.has_perm("build.can_trigger"): + raise PermissionDenied() release_uuid = uuid.uuid4() BuildRelease.objects.create_build_release(release_uuid, release) return HttpResponseRedirect( @@ -77,6 +81,7 @@ def build_release(request, release): @login_required @require_http_methods(["POST"]) +@permission_required("build.can_trigger_hotfix", raise_exception=True) def hotfix_build(request, branch, project): if project not in settings.RELEASE_DASHBOARD_PROJECTS: error = "repo:%s not valid" % project @@ -104,6 +109,7 @@ def hotfix_build(request, branch, project): @login_required +@permission_required("build.can_trigger_hotfix", raise_exception=True) def hotfix(request): prj_list = _projects_versions( settings.RELEASE_DASHBOARD_PROJECTS, regex_hotfix @@ -114,6 +120,7 @@ def hotfix(request): @login_required @require_http_methods(["POST"]) +@permission_required("build.can_trigger_hotfix", raise_exception=True) def hotfix_release_build(request, release, project): release_config = ReleaseConfig(release) if project not in release_config.projects: @@ -138,6 +145,7 @@ def hotfix_release_build(request, release, project): @login_required +@permission_required("build.can_trigger_hotfix", raise_exception=True) def hotfix_release(request, release): release_config = ReleaseConfig(release) if not regex_hotfix.match(release_config.branch): @@ -164,6 +172,7 @@ def refresh_all(request): return render(request, template, {"projects": projects}) +@login_required @require_http_methods(["POST"]) def refresh(request, project): res = gerrit_fetch_info.delay(project) diff --git a/repoapi/migrations/0012_ldap_grp_perms.py b/repoapi/migrations/0012_ldap_grp_perms.py new file mode 100644 index 0000000..0aab445 --- /dev/null +++ b/repoapi/migrations/0012_ldap_grp_perms.py @@ -0,0 +1,86 @@ +# Generated by Django 3.2.13 on 2022-06-14 16:12 +from django.contrib.auth.management import create_permissions +from django.db import migrations + + +def add_permissions(apps, schema_editor): + """ContentType table is populated after all the migrations applied""" + for app_config in apps.get_app_configs(): + app_config.models_module = True + create_permissions(app_config, verbosity=0) + app_config.models_module = None + + +def reverse_func(apps, schema_editor): + add_permissions(apps, schema_editor) + Group = apps.get_model("auth", "Group") + Permission = apps.get_model("auth", "Permission") + ContentType = apps.get_model("contenttypes", "ContentType") + db_alias = schema_editor.connection.alias + + dev_grp = Group.objects.using(db_alias).get(name="dev") + devops_grp = Group.objects.using(db_alias).get(name="devops") + + BuildRelease = apps.get_model("build", "BuildRelease") + ct = ContentType.objects.get_for_model(BuildRelease) + + # these are the wrong ones!! + devops_grp.permissions.set( + [ + Permission.objects.using(db_alias).get( + content_type=ct, codename="can_trigger_hotfix" + ), + ] + ) + dev_grp.permissions.set( + [ + Permission.objects.using(db_alias).get( + content_type=ct, codename="can_trigger" + ), + ] + ) + + +def forwards_func(apps, schema_editor): + add_permissions(apps, schema_editor) + Group = apps.get_model("auth", "Group") + Permission = apps.get_model("auth", "Permission") + ContentType = apps.get_model("contenttypes", "ContentType") + db_alias = schema_editor.connection.alias + + dev_grp = Group.objects.using(db_alias).get(name="dev") + devops_grp = Group.objects.using(db_alias).get(name="devops") + + BuildRelease = apps.get_model("build", "BuildRelease") + ct = ContentType.objects.get_for_model(BuildRelease) + + perm_codenames = ["can_trigger", "can_trigger_hotfix"] + for perm in ("add", "change", "delete", "view"): + perm_codenames.append(f"{perm}_buildrelease") + + perms = [] + for codename in perm_codenames: + perm = Permission.objects.using(db_alias).get( + content_type=ct, codename=codename + ) + perms.append(perm) + devops_grp.permissions.set(perms) + + dev_grp.permissions.set( + [ + Permission.objects.using(db_alias).get( + content_type=ct, codename="can_trigger_hotfix" + ), + ] + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("repoapi", "0011_ldap_groups"), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ]