Skip to content

Commit

Permalink
fixup! wip feat(api): add token domain policy -- TODO: benchmark
Browse files Browse the repository at this point in the history
  • Loading branch information
peterthomassen committed Jul 2, 2021
1 parent 6f17974 commit abc6ecd
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 4 deletions.
1 change: 1 addition & 0 deletions api/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
'desecapi.apps.AppConfig',
'corsheaders',
'django_prometheus',
'pgtrigger',
)

MIDDLEWARE = (
Expand Down
26 changes: 26 additions & 0 deletions api/desecapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from hashlib import sha256

import dns
import pgtrigger
import psl_dns
import rest_framework.authtoken.models
from django.conf import settings
Expand Down Expand Up @@ -456,6 +457,31 @@ def make_hash(plain):
return make_password(plain, salt='static', hasher='pbkdf2_sha256_iter1')


@pgtrigger.register(
# Trigger `condition` arguments (corresponding to WHEN clause) don't support subqueries, so we mostly use `func`
pgtrigger.Trigger(
name='default_policy_on_insert',
operation=pgtrigger.Insert,
when=pgtrigger.Before,
func="IF (NEW.domain_id IS NOT NULL and NOT EXISTS(SELECT * FROM desecapi_tokendomainpolicy WHERE domain_id IS NULL AND token_id = NEW.token_id)) THEN "
" RAISE EXCEPTION 'Cannot insert non-default policy into % table when default policy is not present', TG_TABLE_NAME; "
"END IF; RETURN NEW;",
),
pgtrigger.Protect(
name='default_policy_on_update',
operation=pgtrigger.Update,
when=pgtrigger.Before,
condition=pgtrigger.Q(old__domain__isnull=True, new__domain__isnull=False),
),
pgtrigger.Trigger(
name='default_policy_on_delete',
operation=pgtrigger.Delete,
when=pgtrigger.Before,
func="IF (OLD.domain_id IS NULL and EXISTS(SELECT * FROM desecapi_tokendomainpolicy WHERE domain_id IS NOT NULL AND token_id = OLD.token_id)) THEN "
" RAISE EXCEPTION 'Cannot delete default policy from % table when non-default policy is present', TG_TABLE_NAME; "
"END IF; RETURN OLD;",
),
)
class TokenDomainPolicy(ExportModelOperationsMixin('TokenDomainPolicy'), models.Model):
token = models.ForeignKey(Token, on_delete=models.CASCADE)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, null=True)
Expand Down
13 changes: 13 additions & 0 deletions api/desecapi/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.contrib.auth.password_validation import validate_password
from django.core.validators import MinValueValidator
from django.db.models import Model, Q
from django.db.utils import InternalError
from django.utils import timezone
from netfields import rest_framework as netfields_rf
from rest_framework import fields, serializers
Expand Down Expand Up @@ -76,12 +77,24 @@ def get_fields(self):


class TokenDomainPolicySerializer(serializers.ModelSerializer):
# TODO objects.all()?
domain = serializers.SlugRelatedField(allow_null=True, queryset=models.Domain.objects.all(), slug_field='name')

class Meta:
model = models.TokenDomainPolicy
fields = ('domain', 'perm_dyndns', 'perm_other',)
read_only_fields = ('domain',)

def to_internal_value(self, data):
return {**super().to_internal_value(data),
'token': self.context['request'].user.token_set.get(id=self.context['view'].kwargs['id'])}

def save(self, **kwargs):
try:
return super().save(**kwargs)
except InternalError:
raise serializers.ValidationError('Policy precedence: A default policy must exist before others.') # TODO dict


class RequiredOnPartialUpdateCharField(serializers.CharField):
"""
Expand Down
9 changes: 8 additions & 1 deletion api/desecapi/urls/version_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@

# Token management
path('tokens/', include(tokens_router.urls)),
path('tokens/<id>/domain_policies/', include(tokendomainpolicies_router.urls))
path('tokens/<id>/domain_policies/', include(tokendomainpolicies_router.urls)),
# TODO why is this line necessary?
path('tokens/<id>/domain_policies/<domain__name>/', views.TokenDomainPolicyViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})),
]

domains_router = SimpleRouter()
Expand Down
17 changes: 14 additions & 3 deletions api/desecapi/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,25 @@ def perform_create(self, serializer):


class TokenDomainPolicyViewSet(viewsets.ModelViewSet):
serializer_class = serializers.TokenDomainPolicySerializer
lookup_field = 'domain__name'
pagination_class = None
permission_classes = (IsAuthenticated,)
serializer_class = serializers.TokenDomainPolicySerializer
throttle_scope = 'account_management_passive'

def dispatch(self, request, *args, **kwargs):
# map default policy onto domain_id IS NULL
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
try:
if kwargs[lookup_url_kwarg] == 'default':
kwargs[lookup_url_kwarg] = None
except KeyError:
pass
return super().dispatch(request, *args, **kwargs)

def get_queryset(self):
### TODO token manage permission?
print('============= ID', self.kwargs['id'])
return self.request.user.token_set.get(id=self.kwargs['id']).domain_policies
return models.TokenDomainPolicy.objects.filter(token_id=self.kwargs['id'], token__user=self.request.user)


class DomainViewSet(IdempotentDestroyMixin,
Expand Down
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ django-cors-headers~=3.7.0
djangorestframework~=3.12.2
django-celery-email~=3.0.0
django-netfields~=1.2.2
django-pgtrigger~=2.3.0
django-prometheus~=2.1.0
dnspython~=2.1.0
httpretty~=1.0.2
Expand Down

0 comments on commit abc6ecd

Please sign in to comment.