Skip to content

Commit 5a9f5af

Browse files
committed
chore(perms): add object perms
1 parent fbd0ab0 commit 5a9f5af

11 files changed

Lines changed: 269 additions & 154 deletions

File tree

rootfs/api/consumers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from channels.generic.websocket import AsyncWebsocketConsumer
2121

2222
from .models.app import App
23-
from .permissions import has_app_permission
23+
from .permissions import has_object_permission
2424

2525

2626
class BaseAppConsumer(AsyncWebsocketConsumer):
@@ -35,7 +35,7 @@ def has_perm(self):
3535
if permission is None:
3636
try:
3737
app = App.objects.get(id=self.id)
38-
permission = has_app_permission(self.scope["user"], app, "GET")
38+
permission = has_object_permission(self.scope["user"], app, "GET")
3939
if permission[0]:
4040
cache.set(key, permission, timeout=self.timeout)
4141
except App.DoesNotExist:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.15 on 2024-08-14 09:43
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0009_remove_appsettings_canaries_remove_release_canary_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='certificate',
15+
options={'ordering': ['name', 'common_name', 'expires'], 'permissions': (('use_cert', 'Can use cert'),)},
16+
),
17+
]

rootfs/api/models/app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
from .tls import TLS
3232
from .appsettings import AppSettings
3333
from .volume import Volume
34-
from .base import UuidAuditedModel, PROCFILE_TYPE_WEB, PROCFILE_TYPE_RUN, DEFAULT_HTTP_PORT
34+
from .base import UuidAuditedModel, PROCFILE_TYPE_WEB, PROCFILE_TYPE_RUN, DEFAULT_HTTP_PORT, \
35+
ObjectPolicy, object_policy_registry
3536

3637
User = get_user_model()
3738
logger = logging.getLogger(__name__)
39+
app_policy = ObjectPolicy('id', 'use_app', 'Can use app') # query field, policy, description
3840

3941

4042
# http://kubernetes.io/v1.1/docs/design/identifiers.html
@@ -77,7 +79,7 @@ class App(UuidAuditedModel):
7779

7880
class Meta:
7981
verbose_name = 'Application'
80-
permissions = (('use_app', 'Can use app'),)
82+
permissions = (app_policy[1:], )
8183
ordering = ['id']
8284

8385
def save(self, *args, **kwargs):
@@ -1161,3 +1163,7 @@ def _gather_app_settings(self, release, app_settings, procfile_type, replicas, v
11611163
'pod_security_context': limit_plan.pod_security_context,
11621164
'container_security_context': limit_plan.container_security_context,
11631165
}
1166+
1167+
1168+
# Register policy
1169+
object_policy_registry.register(App, app_policy)

rootfs/api/models/base.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import importlib
55
from datetime import timedelta
66
from functools import partial
7+
from collections import namedtuple
78
from django.db import models
89
from django.conf import settings
910
from django.utils import timezone
@@ -38,6 +39,38 @@
3839
def get_anonymous_user_instance(user): return user(id=-1, username=settings.ANONYMOUS_USER_NAME)
3940

4041

42+
ObjectPolicy = namedtuple('ObjectPolicy', ['unique', 'codename', 'description'])
43+
44+
45+
class ObjectPolicyRegistry(object):
46+
47+
def __init__(self):
48+
self.object_permission_policy_table = dict[type, ObjectPolicy]()
49+
50+
def all(self) -> list[type, ObjectPolicy]:
51+
return self.object_permission_policy_table.items()
52+
53+
def get(self, query) -> tuple[type, ObjectPolicy]:
54+
if isinstance(query, str):
55+
for key, value in self.object_permission_policy_table.items():
56+
if value.codename == query:
57+
return key, value
58+
elif isinstance(query, models.Model):
59+
query = type(query)
60+
value = self.object_permission_policy_table.get(query)
61+
return query if value else None, value
62+
elif isinstance(query, type):
63+
value = self.object_permission_policy_table.get(query)
64+
return query if value else None, value
65+
return None, None
66+
67+
def register(self, cls: type, object_policy: ObjectPolicy) -> None:
68+
self.object_permission_policy_table[cls] = object_policy
69+
70+
71+
object_policy_registry = ObjectPolicyRegistry()
72+
73+
4174
class AuditedModel(models.Model):
4275
"""Add created and updated fields to a model."""
4376

rootfs/api/models/certificate.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
from api.utils import validate_label
1616
from api.exceptions import AlreadyExists, ServiceUnavailable
1717
from scheduler import KubeException
18-
from .base import AuditedModel
18+
from .base import AuditedModel, ObjectPolicy, object_policy_registry
1919
from .domain import Domain
2020

2121
User = get_user_model()
2222
logger = logging.getLogger(__name__)
23+
cert_policy = ObjectPolicy('name', 'use_cert', 'Can use cert')
2324

2425

2526
# Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
@@ -120,6 +121,7 @@ class Certificate(AuditedModel):
120121
subject = models.TextField(editable=False)
121122

122123
class Meta:
124+
permissions = (cert_policy[1:], )
123125
ordering = ['name', 'common_name', 'expires']
124126

125127
@property
@@ -238,3 +240,7 @@ def detach(self, *args, **kwargs):
238240
raise ServiceUnavailable(
239241
"Could not delete certificate secret {} for application {}".format(
240242
self.certname, namespace)) from e
243+
244+
245+
# Register policy
246+
object_policy_registry.register(Certificate, cert_policy)

rootfs/api/permissions.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,38 @@
33
from django.conf import settings
44
from django.contrib.auth.models import AnonymousUser
55
from api import manager
6-
from api.models.blocklist import Blocklist, App
6+
from api.models import app
7+
from api.models import blocklist
8+
from api.models.base import object_policy_registry
79

810

911
def get_app_status(app):
10-
blocklist = Blocklist.get_blocklist(app)
11-
if blocklist:
12-
return False, blocklist.remark
12+
block = blocklist.Blocklist.get_blocklist(app)
13+
if block:
14+
return False, block.remark
1315
if settings.WORKFLOW_MANAGER_URL:
1416
status = manager.User().get_status(app.owner.pk)
1517
if not status["is_active"]:
1618
return False, status["message"]
1719
return True, None
1820

1921

20-
def has_app_permission(user, obj, method):
21-
if isinstance(obj, App) or hasattr(obj, 'app'):
22-
app = obj if isinstance(obj, App) else obj.app
23-
is_ok, message = get_app_status(app)
24-
if is_ok:
25-
if user.is_superuser:
26-
return True, None
27-
elif app.owner == user:
28-
return True, None
29-
elif user.is_staff or user.has_perm('use_app', app):
30-
if method != 'DELETE':
31-
return True, None
32-
else:
33-
return False, "User does not have permission to delete"
22+
def has_object_permission(user, obj, method):
23+
obj = getattr(obj, 'app', obj)
24+
object_policy = object_policy_registry.get(obj)[1]
25+
has_permission, message = False, f"{obj} object does not exist or does not have permission."
26+
if user.is_superuser:
27+
has_permission, message = True, None
28+
elif getattr(obj, "owner", None) == user:
29+
has_permission, message = True, None
30+
elif user.is_staff or (object_policy and user.has_perm(object_policy.codename, obj)):
31+
if method != 'DELETE':
32+
has_permission, message = True, None
3433
else:
35-
return is_ok, message
36-
return False, "App object does not exist or does not have permission."
34+
has_permission, message = False, "{user} does not have permission to delete."
35+
elif has_permission and isinstance(obj, app.App):
36+
return get_app_status(obj)
37+
return has_permission, message
3738

3839

3940
class IsAnonymous(permissions.BasePermission):
@@ -75,13 +76,13 @@ def has_object_permission(self, request, view, obj):
7576
return False
7677

7778

78-
class IsAppUser(permissions.BasePermission):
79+
class IsObjectUser(permissions.BasePermission):
7980
"""
8081
Object-level permission to allow owners or collaborators to access
8182
an app-related model.
8283
"""
8384
def has_object_permission(self, request, view, obj):
84-
return has_app_permission(request.user, obj, request.method)[0]
85+
return has_object_permission(request.user, obj, request.method)[0]
8586

8687

8788
class IsAdmin(permissions.BasePermission):

rootfs/api/tests/test_app.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from api.exceptions import DryccException
2424
from api.tests import adapter, DryccTestCase
25+
from api.models.base import object_policy_registry
2526
import requests_mock
2627

2728
User = get_user_model()
@@ -358,9 +359,12 @@ def test_app_transfer(self, mock_requests):
358359
self.assertEqual(config2.owner, collaborator)
359360

360361
# Collaborators can't transfer
361-
body = {'username': owner.username}
362-
perms_url = url+"/perms/"
363-
response = self.client.post(perms_url, body)
362+
body = {
363+
'username': owner.username,
364+
'codename': object_policy_registry.get(App)[1].codename,
365+
'uniqueid': app.id,
366+
}
367+
response = self.client.post('/v2/perms/rules/', body)
364368
self.assertEqual(response.status_code, 201, response.data)
365369

366370
self.client.credentials(HTTP_AUTHORIZATION='Token ' + owner_token)

rootfs/api/tests/test_hooks.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from django.core.cache import cache
99

1010
from api.tests import adapter, DryccTransactionTestCase
11+
from api.models.app import App
12+
from api.models.base import object_policy_registry
1113
import requests_mock
1214

1315
User = get_user_model()
@@ -70,8 +72,12 @@ def test_key_hook(self, mock_requests):
7072
app_id = self.create_app()
7173

7274
# give user permission to app
73-
url = "/v2/apps/{}/perms".format(app_id)
74-
body = {'username': str(self.user)}
75+
body = {
76+
'username': str(self.user),
77+
'codename': object_policy_registry.get(App)[1].codename,
78+
'uniqueid': app_id,
79+
}
80+
url = "/v2/perms/rules"
7581
response = self.client.post(url, body)
7682
self.assertEqual(response.status_code, 201, response.data)
7783

0 commit comments

Comments
 (0)