From 5a9f5af79d2fdf864a8754345907b0d6b6ce24bd Mon Sep 17 00:00:00 2001 From: duanhongyi Date: Tue, 13 Aug 2024 10:26:53 +0800 Subject: [PATCH] chore(perms): add object perms --- rootfs/api/consumers.py | 4 +- .../0010_alter_certificate_options.py | 17 ++ rootfs/api/models/app.py | 10 +- rootfs/api/models/base.py | 33 ++++ rootfs/api/models/certificate.py | 8 +- rootfs/api/permissions.py | 45 +++--- rootfs/api/tests/test_app.py | 10 +- rootfs/api/tests/test_hooks.py | 10 +- rootfs/api/tests/test_perm.py | 113 +++++++++---- rootfs/api/urls.py | 20 +-- rootfs/api/views.py | 153 +++++++++--------- 11 files changed, 269 insertions(+), 154 deletions(-) create mode 100644 rootfs/api/migrations/0010_alter_certificate_options.py diff --git a/rootfs/api/consumers.py b/rootfs/api/consumers.py index e1552043d..a04186050 100644 --- a/rootfs/api/consumers.py +++ b/rootfs/api/consumers.py @@ -20,7 +20,7 @@ from channels.generic.websocket import AsyncWebsocketConsumer from .models.app import App -from .permissions import has_app_permission +from .permissions import has_object_permission class BaseAppConsumer(AsyncWebsocketConsumer): @@ -35,7 +35,7 @@ def has_perm(self): if permission is None: try: app = App.objects.get(id=self.id) - permission = has_app_permission(self.scope["user"], app, "GET") + permission = has_object_permission(self.scope["user"], app, "GET") if permission[0]: cache.set(key, permission, timeout=self.timeout) except App.DoesNotExist: diff --git a/rootfs/api/migrations/0010_alter_certificate_options.py b/rootfs/api/migrations/0010_alter_certificate_options.py new file mode 100644 index 000000000..fd5e60f4b --- /dev/null +++ b/rootfs/api/migrations/0010_alter_certificate_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.15 on 2024-08-14 09:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0009_remove_appsettings_canaries_remove_release_canary_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='certificate', + options={'ordering': ['name', 'common_name', 'expires'], 'permissions': (('use_cert', 'Can use cert'),)}, + ), + ] diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 84ed5cf1e..515ce2d33 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -31,10 +31,12 @@ from .tls import TLS from .appsettings import AppSettings from .volume import Volume -from .base import UuidAuditedModel, PROCFILE_TYPE_WEB, PROCFILE_TYPE_RUN, DEFAULT_HTTP_PORT +from .base import UuidAuditedModel, PROCFILE_TYPE_WEB, PROCFILE_TYPE_RUN, DEFAULT_HTTP_PORT, \ + ObjectPolicy, object_policy_registry User = get_user_model() logger = logging.getLogger(__name__) +app_policy = ObjectPolicy('id', 'use_app', 'Can use app') # query field, policy, description # http://kubernetes.io/v1.1/docs/design/identifiers.html @@ -77,7 +79,7 @@ class App(UuidAuditedModel): class Meta: verbose_name = 'Application' - permissions = (('use_app', 'Can use app'),) + permissions = (app_policy[1:], ) ordering = ['id'] def save(self, *args, **kwargs): @@ -1161,3 +1163,7 @@ def _gather_app_settings(self, release, app_settings, procfile_type, replicas, v 'pod_security_context': limit_plan.pod_security_context, 'container_security_context': limit_plan.container_security_context, } + + +# Register policy +object_policy_registry.register(App, app_policy) diff --git a/rootfs/api/models/base.py b/rootfs/api/models/base.py index 87ae5353a..038de2970 100644 --- a/rootfs/api/models/base.py +++ b/rootfs/api/models/base.py @@ -4,6 +4,7 @@ import importlib from datetime import timedelta from functools import partial +from collections import namedtuple from django.db import models from django.conf import settings from django.utils import timezone @@ -38,6 +39,38 @@ def get_anonymous_user_instance(user): return user(id=-1, username=settings.ANONYMOUS_USER_NAME) +ObjectPolicy = namedtuple('ObjectPolicy', ['unique', 'codename', 'description']) + + +class ObjectPolicyRegistry(object): + + def __init__(self): + self.object_permission_policy_table = dict[type, ObjectPolicy]() + + def all(self) -> list[type, ObjectPolicy]: + return self.object_permission_policy_table.items() + + def get(self, query) -> tuple[type, ObjectPolicy]: + if isinstance(query, str): + for key, value in self.object_permission_policy_table.items(): + if value.codename == query: + return key, value + elif isinstance(query, models.Model): + query = type(query) + value = self.object_permission_policy_table.get(query) + return query if value else None, value + elif isinstance(query, type): + value = self.object_permission_policy_table.get(query) + return query if value else None, value + return None, None + + def register(self, cls: type, object_policy: ObjectPolicy) -> None: + self.object_permission_policy_table[cls] = object_policy + + +object_policy_registry = ObjectPolicyRegistry() + + class AuditedModel(models.Model): """Add created and updated fields to a model.""" diff --git a/rootfs/api/models/certificate.py b/rootfs/api/models/certificate.py index 45c473a99..7c002d3e3 100644 --- a/rootfs/api/models/certificate.py +++ b/rootfs/api/models/certificate.py @@ -15,11 +15,12 @@ from api.utils import validate_label from api.exceptions import AlreadyExists, ServiceUnavailable from scheduler import KubeException -from .base import AuditedModel +from .base import AuditedModel, ObjectPolicy, object_policy_registry from .domain import Domain User = get_user_model() logger = logging.getLogger(__name__) +cert_policy = ObjectPolicy('name', 'use_cert', 'Can use cert') # Note: This is a slightly bug-fixed version of same from ndg-httpsclient. @@ -120,6 +121,7 @@ class Certificate(AuditedModel): subject = models.TextField(editable=False) class Meta: + permissions = (cert_policy[1:], ) ordering = ['name', 'common_name', 'expires'] @property @@ -238,3 +240,7 @@ def detach(self, *args, **kwargs): raise ServiceUnavailable( "Could not delete certificate secret {} for application {}".format( self.certname, namespace)) from e + + +# Register policy +object_policy_registry.register(Certificate, cert_policy) diff --git a/rootfs/api/permissions.py b/rootfs/api/permissions.py index f7fcab261..8ea9444ea 100644 --- a/rootfs/api/permissions.py +++ b/rootfs/api/permissions.py @@ -3,13 +3,15 @@ from django.conf import settings from django.contrib.auth.models import AnonymousUser from api import manager -from api.models.blocklist import Blocklist, App +from api.models import app +from api.models import blocklist +from api.models.base import object_policy_registry def get_app_status(app): - blocklist = Blocklist.get_blocklist(app) - if blocklist: - return False, blocklist.remark + block = blocklist.Blocklist.get_blocklist(app) + if block: + return False, block.remark if settings.WORKFLOW_MANAGER_URL: status = manager.User().get_status(app.owner.pk) if not status["is_active"]: @@ -17,23 +19,22 @@ def get_app_status(app): return True, None -def has_app_permission(user, obj, method): - if isinstance(obj, App) or hasattr(obj, 'app'): - app = obj if isinstance(obj, App) else obj.app - is_ok, message = get_app_status(app) - if is_ok: - if user.is_superuser: - return True, None - elif app.owner == user: - return True, None - elif user.is_staff or user.has_perm('use_app', app): - if method != 'DELETE': - return True, None - else: - return False, "User does not have permission to delete" +def has_object_permission(user, obj, method): + obj = getattr(obj, 'app', obj) + object_policy = object_policy_registry.get(obj)[1] + has_permission, message = False, f"{obj} object does not exist or does not have permission." + if user.is_superuser: + has_permission, message = True, None + elif getattr(obj, "owner", None) == user: + has_permission, message = True, None + elif user.is_staff or (object_policy and user.has_perm(object_policy.codename, obj)): + if method != 'DELETE': + has_permission, message = True, None else: - return is_ok, message - return False, "App object does not exist or does not have permission." + has_permission, message = False, "{user} does not have permission to delete." + elif has_permission and isinstance(obj, app.App): + return get_app_status(obj) + return has_permission, message class IsAnonymous(permissions.BasePermission): @@ -75,13 +76,13 @@ def has_object_permission(self, request, view, obj): return False -class IsAppUser(permissions.BasePermission): +class IsObjectUser(permissions.BasePermission): """ Object-level permission to allow owners or collaborators to access an app-related model. """ def has_object_permission(self, request, view, obj): - return has_app_permission(request.user, obj, request.method)[0] + return has_object_permission(request.user, obj, request.method)[0] class IsAdmin(permissions.BasePermission): diff --git a/rootfs/api/tests/test_app.py b/rootfs/api/tests/test_app.py index 80adf01dc..25b81da22 100644 --- a/rootfs/api/tests/test_app.py +++ b/rootfs/api/tests/test_app.py @@ -22,6 +22,7 @@ from api.exceptions import DryccException from api.tests import adapter, DryccTestCase +from api.models.base import object_policy_registry import requests_mock User = get_user_model() @@ -358,9 +359,12 @@ def test_app_transfer(self, mock_requests): self.assertEqual(config2.owner, collaborator) # Collaborators can't transfer - body = {'username': owner.username} - perms_url = url+"/perms/" - response = self.client.post(perms_url, body) + body = { + 'username': owner.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app.id, + } + response = self.client.post('/v2/perms/rules/', body) self.assertEqual(response.status_code, 201, response.data) self.client.credentials(HTTP_AUTHORIZATION='Token ' + owner_token) diff --git a/rootfs/api/tests/test_hooks.py b/rootfs/api/tests/test_hooks.py index 432c46a99..81923dd2d 100644 --- a/rootfs/api/tests/test_hooks.py +++ b/rootfs/api/tests/test_hooks.py @@ -8,6 +8,8 @@ from django.core.cache import cache from api.tests import adapter, DryccTransactionTestCase +from api.models.app import App +from api.models.base import object_policy_registry import requests_mock User = get_user_model() @@ -70,8 +72,12 @@ def test_key_hook(self, mock_requests): app_id = self.create_app() # give user permission to app - url = "/v2/apps/{}/perms".format(app_id) - body = {'username': str(self.user)} + body = { + 'username': str(self.user), + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } + url = "/v2/perms/rules" response = self.client.post(url, body) self.assertEqual(response.status_code, 201, response.data) diff --git a/rootfs/api/tests/test_perm.py b/rootfs/api/tests/test_perm.py index 2969859c0..80cf8206f 100644 --- a/rootfs/api/tests/test_perm.py +++ b/rootfs/api/tests/test_perm.py @@ -1,11 +1,13 @@ from django.test import tag from django.contrib.auth import get_user_model from api.tests import DryccTestCase +from api.models.base import object_policy_registry +from api.models.app import App User = get_user_model() -class TestAppPerms(DryccTestCase): +class TestUserPerm(DryccTestCase): fixtures = ['test_sharing.json'] @@ -21,6 +23,18 @@ def setUp(self): self.user3 = User.objects.get(username='autotest-3') self.token3 = self.get_or_create_token(self.user3) + @tag('auth') + def test_codes(self): + response = self.client.get('/v2/perms/codes/') + expect = { + 'results': [ + {'codename': 'use_app', 'description': 'Can use app'}, + {'codename': 'use_cert', 'description': 'Can use cert'} + ], + 'count': 2 + } + self.assertEqual(expect, response.json()) + @tag('auth') def test_create(self): # check that user 1 sees her lone app and user 2's app @@ -43,13 +57,19 @@ def test_create(self): self.assertEqual(response.data['detail'], 'You do not have permission to perform this action.', msg=msg) - # TODO: test that git pushing to the app fails - # give user 2 permission to user 1's app - url = "/v2/apps/{}/perms".format(app_id) - body = {'username': 'autotest-2'} - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) - response = self.client.post(url, body) - self.assertEqual(response.status_code, 201, response.data) + url = "/v2/perms/rules/" + for cls, object_policy in object_policy_registry.all(): + if not cls.objects.exists(): + continue + for obj in cls.objects.filter(owner=self.user): + body = { + 'username': self.user2.username, + 'codename': object_policy.codename, + 'uniqueid': getattr(obj, object_policy.unique) + } + self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) + response = self.client.post(url, body) + self.assertEqual(response.status_code, 201, response.data) # check that user 2 can see the app self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token2) @@ -71,11 +91,15 @@ def test_create_errors(self): response = self.client.get('/v2/apps') app_id = response.data['results'][0]['id'] + body = { + 'username': self.user2.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } + # check that user 2 can't create a permission - url = "/v2/apps/{}/perms".format(app_id) - body = {'username': 'autotest-2'} self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token2) - response = self.client.post(url, body) + response = self.client.post("/v2/perms/rules/", body) self.assertEqual(response.status_code, 403) @tag('auth') @@ -83,9 +107,12 @@ def test_delete(self): # give user 2 permission to user 1's app response = self.client.get('/v2/apps') app_id = response.data['results'][0]['id'] - url = "/v2/apps/{}/perms".format(app_id) - body = {'username': 'autotest-2'} - response = self.client.post(url, body) + body = { + 'username': self.user2.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } + response = self.client.post("/v2/perms/rules/", body) self.assertEqual(response.status_code, 201, response.data) # check that user 2 can see the app as well as his own @@ -96,7 +123,8 @@ def test_delete(self): # delete permission to user 1's app self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) - url = "/v2/apps/{}/perms/{}".format(app_id, 'autotest-2') + response = self.client.get("/v2/perms/rules/") + url = "/v2/perms/rules/{}/".format(response.json()["results"][0]["id"]) response = self.client.delete(url) self.assertEqual(response.status_code, 204, response.data) self.assertIsNone(response.data) @@ -109,7 +137,7 @@ def test_delete(self): # delete permission to user 1's app again, expecting an error self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) response = self.client.delete(url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) @tag('auth') def test_list(self): @@ -120,14 +148,17 @@ def test_list(self): app_id = response.data['results'][0]['id'] # create a new object permission - url = "/v2/apps/{}/perms".format(app_id) - body = {'username': 'autotest-2'} - response = self.client.post(url, body) + body = { + 'username': self.user2.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } + response = self.client.post("/v2/perms/rules/", body) self.assertEqual(response.status_code, 201, response.data) # list perms on the app - response = self.client.get("/v2/apps/{}/perms".format(app_id)) - self.assertEqual(response.data, {'users': ['autotest-2']}) + response = self.client.get("/v2/perms/rules/") + self.assertEqual(response.data["count"], 1) @tag('auth') def test_admin_can_list(self): @@ -139,12 +170,11 @@ def test_admin_can_list(self): @tag('auth') def test_list_errors(self): response = self.client.get('/v2/apps') - app_id = response.data['results'][0]['id'] - # login as user 2, list perms on the app self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token2) - response = self.client.get("/v2/apps/{}/perms".format(app_id)) - self.assertEqual(response.status_code, 403) + response = self.client.get("/v2/perms/rules/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, {'results': [], 'count': 0}) @tag('auth') def test_unauthorized_user_cannot_modify_perms(self): @@ -159,10 +189,13 @@ def test_unauthorized_user_cannot_modify_perms(self): body = {'id': app_id} response = self.client.post(url, body) - url = '{}/{}/perms'.format(url, app_id) - body = {'username': self.user2.username} + body = { + 'username': self.user2.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token2) - response = self.client.post(url, body) + response = self.client.post('/v2/perms/rules/', body) self.assertEqual(response.status_code, 403) @tag('auth') @@ -174,17 +207,25 @@ def test_collaborator_cannot_share(self): owner_token = self.token collab = self.user2 collab_token = self.token2 - url = '/v2/apps/{}/perms'.format(app_id) # Share app with collaborator - body = {'username': collab.username} + body = { + 'username': collab.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } + url = '/v2/perms/rules/' self.client.credentials(HTTP_AUTHORIZATION='Token ' + owner_token) response = self.client.post(url, body) self.assertEqual(response.status_code, 201, response.data) # Collaborator should fail to share app self.client.credentials(HTTP_AUTHORIZATION='Token ' + collab_token) - body = {'username': self.user3.username} + body = { + 'username': self.user3.username, + 'codename': object_policy_registry.get(App)[1].codename, + 'uniqueid': app_id, + } response = self.client.post(url, body) self.assertEqual(response.status_code, 403) @@ -197,16 +238,18 @@ def test_collaborator_cannot_share(self): response = self.client.post(url, body) self.assertEqual(response.status_code, 201, response.data) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + collab_token) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token3) response = self.client.get(url) self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(response.data['count'], 1, response.data) # Collaborator cannot delete other collaborator - url += "/{}".format(self.user3.username) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + collab_token) + url += "{}/".format(response.data["results"][0]["id"]) response = self.client.delete(url) self.assertEqual(response.status_code, 403) # Collaborator can delete themselves - url = '/v2/apps/{}/perms/{}'.format(app_id, collab.username) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token3) response = self.client.delete(url) - self.assertEqual(response.status_code, 204, response.data) + self.assertEqual(response.status_code, 204) diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py index 69fd4b291..312aecf74 100644 --- a/rootfs/api/urls.py +++ b/rootfs/api/urls.py @@ -97,13 +97,6 @@ re_path( r"^apps/(?P{})/tls/?$".format(settings.APP_URL_REGEX), views.TLSViewSet.as_view({'get': 'retrieve', 'post': 'create'})), - # apps sharing - re_path( - r"^apps/(?P{})/perms/(?P[-_\w]+)/?$".format(settings.APP_URL_REGEX), - views.AppPermsViewSet.as_view({'delete': 'destroy'})), - re_path( - r"^apps/(?P{})/perms/?$".format(settings.APP_URL_REGEX), - views.AppPermsViewSet.as_view({'get': 'list', 'post': 'create'})), # application volumes re_path( r"^apps/(?P{})/volumes/?$".format(settings.APP_URL_REGEX), @@ -173,13 +166,6 @@ re_path( r'^auth/whoami/?$', views.UserManagementViewSet.as_view({'get': 'list'})), - # admin sharing - re_path( - r'^admin/perms/(?P[\w.@+-]+)/?$', - views.AdminPermsViewSet.as_view({'delete': 'destroy'})), - re_path( - r'^admin/perms/?$', - views.AdminPermsViewSet.as_view({'get': 'list', 'post': 'create'})), # certificates re_path( r'^certs/(?P[-_*.\w]+)/domain/(?P\**\.?[-\._\w]+)?/?$', @@ -233,6 +219,12 @@ re_path( r'^manager/(?P[\w.@+-]+)s/(?P{})/unblock/?$'.format(settings.APP_URL_REGEX), views.WorkflowManagerViewset.as_view({'delete': 'unblock'})), + # user perms + re_path(r"^perms/codes/?$", views.UserPermViewSet.as_view({'get': 'codes'})), + re_path( + r"^perms/rules/?$", views.UserPermViewSet.as_view({'get': 'list', 'post': 'create'})), + re_path( + r"^perms/rules/(?P[-_\w]+)/?$", views.UserPermViewSet.as_view({'delete': 'destroy'})), # tokens re_path(r'^tokens/?$', views.TokenViewSet.as_view({'get': 'list'})), re_path(r"^tokens/(?P[-_\w]+)/?$", views.TokenViewSet.as_view({'delete': 'destroy'})), diff --git a/rootfs/api/views.py b/rootfs/api/views.py index 96a3cc1c8..5bebce5ce 100644 --- a/rootfs/api/views.py +++ b/rootfs/api/views.py @@ -13,13 +13,12 @@ from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404, redirect from guardian.shortcuts import assign_perm, get_objects_for_user, \ - get_users_with_perms, remove_perm + get_users_with_perms, remove_perm, UserObjectPermission from django.views.generic import View from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers from rest_framework import renderers, status, filters -from rest_framework.exceptions import PermissionDenied, NotFound from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -244,7 +243,7 @@ class BaseDryccViewSet(viewsets.OwnerViewSet): the `model` attribute shortcut. """ lookup_field = 'id' - permission_classes = [IsAuthenticated, permissions.IsAppUser] + permission_classes = [IsAuthenticated, permissions.IsObjectUser] renderer_classes = [renderers.JSONRenderer] @@ -295,7 +294,7 @@ def list(self, request, *args, **kwargs): interact with. """ queryset = super(AppViewSet, self).get_queryset(**kwargs) | \ - get_objects_for_user(self.request.user, 'api.use_app') + get_objects_for_user(self.request.user, f'api.{models.app.app_policy[1]}') instance = self.filter_queryset(queryset) page = self.paginate_queryset(instance) if page is not None: @@ -327,7 +326,7 @@ def update(self, request, **kwargs): if request.data.get('owner'): if self.request.user != app.owner and not self.request.user.is_superuser: - raise PermissionDenied() + return Response(status=status.HTTP_403_FORBIDDEN) new_owner = get_object_or_404(User, username=request.data['owner']) # ensure all downstream objects that are owned by this user and are part of this app # is also updated @@ -575,6 +574,11 @@ class CertificateViewSet(BaseDryccViewSet): model = models.certificate.Certificate serializer_class = serializers.CertificateSerializer + def get_queryset(self, **kwargs): + return self.model.objects.filter(owner=self.request.user, **kwargs) | \ + get_objects_for_user( + self.request.user, f'api.{models.certificate.cert_policy[1]}') + def get_object(self, **kwargs): """Retrieve domain certificate by its name""" qs = self.get_queryset(**kwargs) @@ -598,7 +602,13 @@ def detach(self, request, *args, **kwargs): self.get_object().detach(*args, **kwargs) except Http404: raise + return Response(status=status.HTTP_204_NO_CONTENT) + def destroy(self, *args, **kwargs): + resource = self.get_object() + if self.request.user != resource.owner and not self.request.user.is_superuser: + return Response(status=status.HTTP_403_FORBIDDEN) + resource.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -653,7 +663,7 @@ def public_key(self, request, *args, **kwargs): key = get_object_or_404(models.key.Key, fingerprint=fingerprint) queryset = models.app.App.objects.all() | \ - get_objects_for_user(self.request.user, 'api.use_app') + get_objects_for_user(self.request.user, f'api.{models.app.app_policy[1]}') items = self.filter_queryset(queryset) apps = [] @@ -669,10 +679,8 @@ def public_key(self, request, *args, **kwargs): def app(self, request, *args, **kwargs): app = get_object_or_404(models.app.App, id=kwargs['id']) - - perm_name = "api.use_app" usernames = [u.id for u in get_users_with_perms(app) - if u.has_perm(perm_name, app)] + if u.has_perm(f"api.{models.app.app_policy[1]}", app)] data = {} result = models.key.Key.objects \ @@ -695,10 +703,10 @@ def users(self, request, *args, **kwargs): app = get_object_or_404(models.app.App, id=kwargs['id']) request.user = get_object_or_404(User, username=kwargs['username']) # check the user is authorized for this app - has_permission, message = permissions.has_app_permission( + has_permission, message = permissions.has_object_permission( request.user, app, request.method) if not has_permission: - raise PermissionDenied(message) + return Response(message, status=status.HTTP_403_FORBIDDEN) data = {request.user.username: []} keys = models.key.Key.objects \ @@ -706,7 +714,7 @@ def users(self, request, *args, **kwargs): .values('public', 'fingerprint') \ .order_by('created') if not keys: - raise NotFound("No Keys match the given query.") + return Response("No Keys match the given query.", status=status.HTTP_404_NOT_FOUND) for info in keys: data[request.user.username].append({ @@ -726,9 +734,9 @@ def create(self, request, *args, **kwargs): app = get_object_or_404(models.app.App, id=request.data['receive_repo']) self.user = request.user = get_object_or_404(User, username=request.data['receive_user']) # check the user is authorized for this app - has_permission, message = permissions.has_app_permission(self.user, app, request.method) + has_permission, message = permissions.has_object_permission(self.user, app, request.method) if not has_permission: - raise PermissionDenied(message) + return Response(message, status=status.HTTP_403_FORBIDDEN) request.data['app'] = app request.data['owner'] = self.user super(BuildHookViewSet, self).create(request, *args, **kwargs) @@ -753,56 +761,79 @@ def create(self, request, *args, **kwargs): app = get_object_or_404(models.app.App, id=request.data['receive_repo']) request.user = get_object_or_404(User, username=request.data['receive_user']) # check the user is authorized for this app - has_permission, message = permissions.has_app_permission( + has_permission, message = permissions.has_object_permission( request.user, app, request.method) if not has_permission: - raise PermissionDenied(message) + return Response(message, status=status.HTTP_403_FORBIDDEN) config = app.release_set.filter(failed=False).latest().config serializer = self.get_serializer(config) return Response(serializer.data, status=status.HTTP_200_OK) -class AppPermsViewSet(BaseDryccViewSet): +class UserPermViewSet(BaseDryccViewSet): """RESTful views for sharing apps with collaborators.""" - model = models.app.App # models class - perm = 'use_app' # short name for permission - - def get_queryset(self): - return self.model.objects.all() + def codes(self, request, **kwargs): + results = [] + for _, object_policy in permissions.object_policy_registry.all(): + results.append({ + "codename": object_policy.codename, + "description": object_policy.description, + }) + # fake out pagination for now + pagination = {'results': results, 'count': len(results)} + return Response(data=pagination) def list(self, request, **kwargs): - app = self.get_object() - perm_name = "api.{}".format(self.perm) - usernames = [u.username for u in get_users_with_perms(app) - if u.has_perm(perm_name, app)] - return Response({'users': usernames}) + cls, object_policy = permissions.object_policy_registry.get( + request.query_params.get('codename', '')) + if object_policy: + object_policies = [(cls, object_policy), ] + else: + object_policies = permissions.object_policy_registry.all() + results = [] + for cls, object_policy in object_policies: + user_object_perms = UserObjectPermission.objects.filter( + object_pk__in=[obj.pk for obj in cls.objects.filter(owner=request.user)], + permission__codename=object_policy.codename + ) + user_object_perms |= UserObjectPermission.objects.filter( + user=request.user, permission__codename=object_policy.codename) + + for user_object_perm in user_object_perms: + results.append({ + "id": user_object_perm.pk, + "codename": user_object_perm.permission.codename, + "uniqueid": getattr(user_object_perm.content_object, object_policy.unique), + "username": user_object_perm.user.username, + }) + # fake out pagination for now + pagination = {'results': results, 'count': len(results)} + return Response(data=pagination) def create(self, request, **kwargs): - app = self.get_object() - if not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(), - request, self, app): - raise PermissionDenied() - - user = get_object_or_404(User, username=request.data['username']) - assign_perm(self.perm, user, app) - app.log("User {} was granted access to {}".format(user, app)) + username = request.data.get('username') + codename = request.data.get('codename') + uniqueid = request.data.get('uniqueid') + cls, object_policy = permissions.object_policy_registry.get(codename) + if not cls or not object_policy: + return Response(f"User {codename} not found", status=status.HTTP_404_NOT_FOUND) + obj = get_object_or_404(cls, **{object_policy.unique: uniqueid}) + if not permissions.IsOwnerOrAdmin().has_object_permission(request, self, obj): + return Response(status=status.HTTP_403_FORBIDDEN) + user = get_object_or_404(User, username=username) + assign_perm(object_policy.codename, user, obj) + getattr(obj, "log", logger.info)("User {} was granted access to {}".format(user, obj)) return Response(status=status.HTTP_201_CREATED) def destroy(self, request, **kwargs): - app = get_object_or_404(models.app.App, id=self.kwargs['id']) - user = get_object_or_404(User, username=kwargs['username']) - - perm_name = "api.{}".format(self.perm) - if not user.has_perm(perm_name, app): - raise PermissionDenied() - - if (user != request.user and - not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(), - request, self, app)): - raise PermissionDenied() - remove_perm(self.perm, user, app) - app.log("User {} was revoked access to {}".format(user, app)) + user_object_permission = get_object_or_404(UserObjectPermission, id=self.kwargs['id']) + obj = user_object_permission.content_object + if not (obj.owner == request.user or user_object_permission.user == request.user): + return Response(status=status.HTTP_403_FORBIDDEN) + perm_name = f"api.{user_object_permission.permission.codename}" + remove_perm(perm_name, user_object_permission.user, obj) + obj.log("User {} was revoked access to {}".format(user_object_permission.user, obj)) return Response(status=status.HTTP_204_NO_CONTENT) @@ -975,7 +1006,7 @@ def binding(self, request, *args, **kwargs): logger.info("resoruce unbind response data: {}".format(serializer)) return Response(serializer.data) else: - return Http404("unknown action") + return Response("unknown action", status=status.HTTP_404_NOT_FOUND) class GatewayViewSet(AppResourceViewSet): @@ -1098,30 +1129,6 @@ def delete(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class AdminPermsViewSet(BaseDryccViewSet): - """RESTful views for sharing admin permissions with other users.""" - - model = User - serializer_class = serializers.AdminUserSerializer - permission_classes = [permissions.IsAdmin] - - def get_queryset(self, **kwargs): - self.check_object_permissions(self.request, self.request.user) - return self.model.objects.filter(is_active=True, is_superuser=True) - - def create(self, request, **kwargs): - user = get_object_or_404(self.model, username=request.data['username']) - user.is_superuser = user.is_staff = True - user.save(update_fields=['is_superuser', 'is_staff']) - return Response(status=status.HTTP_201_CREATED) - - def destroy(self, request, **kwargs): - user = get_object_or_404(self.model, username=kwargs['username']) - user.is_superuser = user.is_staff = False - user.save(update_fields=['is_superuser', 'is_staff']) - return Response(status=status.HTTP_204_NO_CONTENT) - - class UserView(BaseDryccViewSet): """A Viewset for interacting with User objects.""" model = User