|
22 | 22 | from django.contrib.admin.models import CHANGE |
23 | 23 | from django.contrib.admin.models import DELETION |
24 | 24 | from django.contrib.admin.models import LogEntry |
25 | | -from django.contrib.auth import get_user_model |
26 | 25 | from django.contrib.auth.models import AbstractUser |
27 | 26 | from django.contrib.auth.models import BaseUserManager |
28 | 27 | from django.contrib.auth.models import Group |
@@ -90,6 +89,41 @@ def is_content_type_related(model_class): |
90 | 89 | ) |
91 | 90 |
|
92 | 91 |
|
| 92 | +class DataspaceForeignKeyValidationMixin: |
| 93 | + """Mixin that enforces all related objects share the same Dataspace as self.""" |
| 94 | + |
| 95 | + def save(self, *args, **kwargs): |
| 96 | + """Validate all foreign keys share the same Dataspace before saving.""" |
| 97 | + self._validate_fk_dataspace() |
| 98 | + super().save(*args, **kwargs) |
| 99 | + |
| 100 | + def _validate_fk_dataspace(self): |
| 101 | + """Check that all foreign key objects share this instance's Dataspace.""" |
| 102 | + # For these model classes, related objects can still be saved even if |
| 103 | + # they have a dataspace which is not the current one. |
| 104 | + allowed_models = [Dataspace, DejacodeUser, ContentType] |
| 105 | + |
| 106 | + for fk_field in self.local_foreign_fields: |
| 107 | + if fk_field.related_model in allowed_models: |
| 108 | + continue |
| 109 | + |
| 110 | + fk_field_value = getattr(self, fk_field.name, None) |
| 111 | + if fk_field_value and fk_field_value.dataspace != self.dataspace: |
| 112 | + raise ValueError( |
| 113 | + f'Foreign key field "{fk_field.name}": related object "{fk_field_value}" ' |
| 114 | + f'has Dataspace "{fk_field_value.dataspace}", expected "{self.dataspace}"' |
| 115 | + ) |
| 116 | + |
| 117 | + def _get_local_foreign_fields(self): |
| 118 | + """ |
| 119 | + Return a list of ForeignKey type fields of the model. |
| 120 | + GenericForeignKey are not included, filtered out with field.concrete |
| 121 | + """ |
| 122 | + return [field for field in self._meta.get_fields() if field.many_to_one and field.concrete] |
| 123 | + |
| 124 | + local_foreign_fields = property(_get_local_foreign_fields) |
| 125 | + |
| 126 | + |
93 | 127 | class DataspaceManager(models.Manager): |
94 | 128 | def get_by_natural_key(self, name): |
95 | 129 | return self.get(name=name) |
@@ -414,7 +448,7 @@ def tab_permissions_enabled(self): |
414 | 448 | return bool(self.get_configuration("tab_permissions")) |
415 | 449 |
|
416 | 450 |
|
417 | | -class DataspaceConfiguration(models.Model): |
| 451 | +class DataspaceConfiguration(DataspaceForeignKeyValidationMixin, models.Model): |
418 | 452 | dataspace = models.OneToOneField( |
419 | 453 | to="dje.Dataspace", |
420 | 454 | on_delete=models.CASCADE, |
@@ -756,7 +790,7 @@ def exclude_locked_products(self): |
756 | 790 | ) |
757 | 791 |
|
758 | 792 |
|
759 | | -class DataspacedModel(models.Model): |
| 793 | +class DataspacedModel(DataspaceForeignKeyValidationMixin, models.Model): |
760 | 794 | """Abstract base model for all models that are keyed by Dataspace.""" |
761 | 795 |
|
762 | 796 | dataspace = models.ForeignKey( |
@@ -848,19 +882,6 @@ def save(self, *args, **kwargs): |
848 | 882 | # It needs to be poped before calling the super().save() |
849 | 883 | kwargs.pop("copy", None) |
850 | 884 |
|
851 | | - # For these model classes, related objects can still be saved even if |
852 | | - # they have a dataspace which is not the current one. |
853 | | - allowed_models = [Dataspace, get_user_model(), ContentType] |
854 | | - |
855 | | - for field in self.local_foreign_fields: |
856 | | - if field.related_model not in allowed_models: |
857 | | - attr_value = getattr(self, field.name) |
858 | | - if attr_value and attr_value.dataspace != self.dataspace: |
859 | | - raise ValueError( |
860 | | - f'The Dataspace of the related object: "{attr_value}" ' |
861 | | - f'is not "{self.dataspace}"' |
862 | | - ) |
863 | | - |
864 | 885 | self.clean_extra_spaces_in_identifier_fields() |
865 | 886 | super().save(*args, **kwargs) |
866 | 887 |
|
@@ -1089,15 +1110,6 @@ def urn_link(self): |
1089 | 1110 | if urn: |
1090 | 1111 | return format_html('<a href="{}">{}</a>', reverse("urn_resolve", args=[urn]), urn) |
1091 | 1112 |
|
1092 | | - def _get_local_foreign_fields(self): |
1093 | | - """ |
1094 | | - Return a list of ForeignKey type fields of the model. |
1095 | | - GenericForeignKey are not included, filtered out with field.concrete |
1096 | | - """ |
1097 | | - return [field for field in self._meta.get_fields() if field.many_to_one and field.concrete] |
1098 | | - |
1099 | | - local_foreign_fields = property(_get_local_foreign_fields) |
1100 | | - |
1101 | 1113 | @classmethod |
1102 | 1114 | def get_identifier_fields(cls, *args, **kwargs): |
1103 | 1115 | """ |
@@ -1678,7 +1690,7 @@ def get_vulnerability_notifications_users(self, dataspace): |
1678 | 1690 | ) |
1679 | 1691 |
|
1680 | 1692 |
|
1681 | | -class DejacodeUser(AbstractUser): |
| 1693 | +class DejacodeUser(DataspaceForeignKeyValidationMixin, AbstractUser): |
1682 | 1694 | uuid = models.UUIDField( |
1683 | 1695 | _("UUID"), |
1684 | 1696 | default=uuid.uuid4, |
|
0 commit comments