Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
peterthomassen committed Nov 21, 2024
1 parent 13240be commit 1056a54
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 1 deletion.
2 changes: 1 addition & 1 deletion api/desecapi/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def authenticate_credentials(self, key):
if not token.is_valid:
raise exceptions.AuthenticationFailed("Invalid token.")
token.last_used = timezone.now()
token.save()
token.save(update_fields=["last_used"])
return user, token


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 5.1.3 on 2024-11-20 16:08

import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("desecapi", "0039_token_perm_create_domain_token_perm_delete_domain"),
]

operations = [
migrations.AddField(
model_name="token",
name="auto_policy",
field=models.BooleanField(default=False),
),
pgtrigger.migrations.AddTrigger(
model_name="token",
trigger=pgtrigger.compiler.Trigger(
name="token_auto_policy",
sql=pgtrigger.compiler.UpsertTriggerSql(
constraint="CONSTRAINT",
func="\n IF\n NEW.auto_policy = true AND NOT EXISTS(\n SELECT * FROM desecapi_tokendomainpolicy WHERE token_id = NEW.id AND domain_id IS NULL AND subname IS NULL AND type IS NULL\n )\n THEN\n RAISE EXCEPTION 'Token auto policy without a default policy is not allowed.';\n END IF;\n RETURN NULL;\n ",
hash="890632f787e37adaed486fb702cc9f929449f04c",
operation="UPDATE OR INSERT",
pgid="pgtrigger_token_auto_policy_8e6d9",
table="desecapi_token",
timing="DEFERRABLE INITIALLY DEFERRED",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="tokendomainpolicy",
trigger=pgtrigger.compiler.Trigger(
name="default_policy_when_auto_policy",
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\n IF\n OLD.domain_id IS NULL AND OLD.subname IS NULL AND OLD.type IS NULL AND (SELECT auto_policy FROM desecapi_token WHERE id = OLD.token_id) = true\n THEN\n RAISE EXCEPTION 'Cannot delete default policy while auto_policy is in effect.';\n END IF;\n RETURN OLD;\n ",
hash="08b8e7ab285d9e5a6bd612ff0704aeaa328438d2",
operation="DELETE",
pgid="pgtrigger_default_policy_when_auto_policy_a1fd2",
table="desecapi_tokendomainpolicy",
when="BEFORE",
),
),
),
]
68 changes: 68 additions & 0 deletions api/desecapi/models/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def _allowed_subnets_default():
null=True, default=None, validators=_validators
)
domain_policies = models.ManyToManyField("Domain", through="TokenDomainPolicy")
auto_policy = models.BooleanField(default=False)

plain = None
objects = NetManager()
Expand All @@ -60,6 +61,27 @@ class Meta:
constraints = [
models.UniqueConstraint(fields=["id", "user"], name="unique_id_user")
]
triggers = [
# Ensure that a default policy is defined when auto_policy=true
pgtrigger.Trigger(
name="token_auto_policy",
operation=pgtrigger.Update | pgtrigger.Insert,
when=pgtrigger.After,
timing=pgtrigger.Deferred,
func=pgtrigger.Func(
"""
IF
NEW.auto_policy = true AND NOT EXISTS(
SELECT * FROM {meta.many_to_many[0].remote_field.through._meta.db_table} WHERE token_id = NEW.id AND domain_id IS NULL AND subname IS NULL AND type IS NULL
)
THEN
RAISE EXCEPTION 'Token auto policy without a default policy is not allowed.';
END IF;
RETURN NULL;
"""
),
),
]

@property
def is_valid(self):
Expand Down Expand Up @@ -108,6 +130,18 @@ def get_policy(self, rrset=None):
.first()
)

def clean(self):
default_policy = self.get_policy()
if self.auto_policy and (default_policy is None or default_policy.perm_write):
raise ValidationError(
{"auto_policy": ["Auto policy requires a restrictive default policy."]}
)

def save(self, *args, **kwargs):
if "auto_policy" in kwargs.get("update_fields", ["auto_policy"]):
self.clean()
super().save(*args, **kwargs)


class TokenDomainPolicy(ExportModelOperationsMixin("TokenDomainPolicy"), models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Expand Down Expand Up @@ -163,6 +197,22 @@ class Meta:
"""
),
),
# Ensure default policy when auto_policy is in effect
pgtrigger.Trigger(
name="default_policy_when_auto_policy",
operation=pgtrigger.Delete,
when=pgtrigger.Before,
func=pgtrigger.Func(
"""
IF
OLD.domain_id IS NULL AND OLD.subname IS NULL AND OLD.type IS NULL AND (SELECT auto_policy FROM {fields.token.remote_field.model._meta.db_table} WHERE id = OLD.token_id) = true
THEN
RAISE EXCEPTION 'Cannot delete default policy while auto_policy is in effect.';
END IF;
RETURN OLD;
"""
),
),
]

@property
Expand Down Expand Up @@ -196,6 +246,15 @@ def clean(self):
]
}
)
# Can't relax default policy if auto_policy is in effect
if self.perm_write and self.is_default_policy and self.token.auto_policy:
raise ValidationError(
{
"perm_write": [
"Must be false when auto_policy is in effect for the token."
]
}
)

def delete(self, *args, **kwargs):
# Can't delete default policy when others exist
Expand All @@ -210,6 +269,15 @@ def delete(self, *args, **kwargs):
]
}
)
# Can't delete default policy when auto_policy is in effect
if self.is_default_policy and self.token.auto_policy:
raise ValidationError(
{
"non_field_errors": [
"Can't delete default policy when auto_policy is in effect for the token."
]
}
)
return super().delete(*args, **kwargs)

def save(self, *args, **kwargs):
Expand Down
7 changes: 7 additions & 0 deletions api/desecapi/serializers/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Meta:
"perm_delete_domain",
"perm_manage_tokens",
"allowed_subnets",
"auto_policy",
"is_valid",
"token",
)
Expand All @@ -38,6 +39,12 @@ def get_fields(self):
fields.pop("token")
return fields

def save(self, **kwargs):
try:
return super().save(**kwargs)
except django.core.exceptions.ValidationError as exc:
raise serializers.ValidationError(exc.message_dict)


class DomainSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
Expand Down
2 changes: 2 additions & 0 deletions api/desecapi/tests/test_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def test_retrieve_my_token(self):
"perm_delete_domain",
"perm_manage_tokens",
"allowed_subnets",
"auto_policy",
"is_valid",
},
)
Expand Down Expand Up @@ -134,6 +135,7 @@ def test_create_token(self):
"perm_delete_domain",
"perm_manage_tokens",
"allowed_subnets",
"auto_policy",
"is_valid",
"token",
},
Expand Down

0 comments on commit 1056a54

Please sign in to comment.