owned this note
owned this note
Published
Linked with GitHub
---
title: Current Domains Development
tags: Pulp, Domains, Multitenancy
description: Current state of Domain development as of 10-3-2022
---
# Domains Current Development State
Oct 3, 2022
pulpcore: https://github.com/pulp/pulpcore/pull/3190
pulp_file: https://github.com/pulp/pulp_file/pull/810/
discourse: https://discourse.pulpproject.org/t/new-multi-tenancy-feature-domains/635/3
---
### Domain Model
```python
class Domain(BaseModel, AutoAddObjPermsMixin):
name = models.TextField(null=False, unique=True)
description = models.TextField(null=True)
# Storage class is required, optional settings are validated by serializer
storage_class = models.TextField(null=False)
storage_settings = models.JSONField(default=dict)
def get_storage(self):
"""Returns this domain's instantiated storage class."""
if self.storage_class == "default":
return default_storage
storage_class = get_storage_class(self.storage_class)
return storage_class(**self.storage_settings)
```
---
### Pulp w/ Domains
```python
domain = models.ForeignKey("Domain", default=get_default_domain, on_delete=PROTECT)
# From pulpcore.app.utils
def get_default_domain():
global default_domain_pk
# This can be run in a migration, and once after
if default_domain_pk is None:
try:
Domain = models.Domain
except AttributeError:
return None
try:
domain = Domain.objects.get(name="default", storage_class="default")
except Domain.DoesNotExist:
domain = Domain(name="default", storage_class="default")
domain.save(skip_hooks=True)
default_domain_pk = domain.pk
return default_domain_pk
```
* [Most objects will now have a domain relation](https://hackmd.io/X8IWzy95SCWnuTV4RPK9JA)
* Default domain is needed to allow domain to become apart of objects `unique_together` constraint
* Notable objects without domains: `User`, `Group`, `AccessPolicy`, `Role`
---
### Enabling Domains in Pulp
While the `Domain` object will always be present in Pulp, the features enabled by domains will be off by default. Turning on domains will be controlled through settings `DOMAIN_ENABLED`.
* Pulp will only start if each plugin is compatible with domains. Done through a new attribute on the `PulpPluginConfig`
```python
class PulpFilePluginAppConfig(PulpPluginAppConfig):
...
domain_compatible = True
```
* API Url routing will change to use new settings `V3_DOMAIN_API_ROOT`
* `API_ROOT/<domain_path>/api/v3/` - e.g. `/pulp/<domain_path>/api/v3/`
* Content app routing will now have `{domain_path}` added to the end of `CONTENT_ORIGIN`
* `CONTENT_ORIGIN/{domain_path}/{path}` -e.g. `/pulp/content/{domain_path}/
* All current objects in Pulp will be found under the `default` domain
* This includes objects that are not apart of domains
---
### Files in Domains
To have files uploaded to their correct domain `FieldFile` used for `Artifact` and `TemporaryFile`'s `FileField` has been customized
```python
class FieldFile(BaseFileField.attr_class):
@property
def storage(self):
if domain := getattr(self.instance, "domain", None):
return domain.get_storage()
return self._storage
@storage.setter
def storage(self, value):
self._storage = value
def get_artifact_path(sha256digest, domain=None):
args = ["artifact", sha256digest[:2], sha256digest[2:]]
# Prevent collisions if two domains have the same backend settings
if domain is not None:
if domain.storage_class != "default":
args.insert(1, str(domain.pulp_id))
return os.path.join(*args)
with artifact.file.open() as f:
...
```
---
### Backwards Compatibility and Hidden Helpers
Adding domain to the URL would change the signatures for all our viewsets, so a new middleware has been added to intercept and remove the `domain_path`, setting it on the request object before calling the viewset's handler.
```python
def process_view(self, request, view_func, view_args, view_kwargs):
domain = view_kwargs.pop("domain", None) or "default"
if not Domain.objects.filter(name=domain).exists():
raise Http404()
setattr(request, "domain", domain)
return None
```
To avoid having to modify every serializer to handle the new domain parameter for object creation a new hidden field has been added to the `ModelSerializer`
```python
class ModelSerializer(serializers.ModelSerializer):
...
def _get_domain(self):
context = self.root.context
if domain := context.get("domain", None):
if isinstance(domain, Domain):
return domain
name = domain
elif request := context.get("request", None):
name = getattr(request, "domain", "default")
else:
name = "default"
return Domain.objects.get(name=name)
domain = serializers.HiddenField(default=_get_domain)
def __init__subclass():
...
if "domain" in meta.fields and model:
if not hasattr(model, "domain"):
meta.fields = tuple(set(meta.fields) - {"domain"})
```
---
### RBAC w/ Domains
There is a new level for permissions: Domain-level, and with it comes new Global Access Conditions.
```python
class UserRole(BaseModel): # Same change on GroupRole
...
domain = models.ForeignKey("Domain", null=True, on_delete=CASCADE)
def has_domain_perms(request, view, action, permission):
if settings.DOMAIN_ENABLED:
domain_name = request.domain
domain = Domain.objects.get(name=domain_name)
return request.user.has_perm(permission, obj=domain)
return False
def has_model_domain_or_obj_perms(...):
return has_model_perms(...) or has_domain_perms(...) or has_object_perms(...)
```
* Roles and AccessPolicies will remain system wide and will be up to the System Admin to determine what is appropriate for users and their domains.
* Objects will be scoped to their domain on top of RBAC scoping
* Currently endpoints/objects without domains are interactable in every domain (to users with correct permissions), but their href will always show as being in the 'defualt' domain
---
### Adding Domain Compatibility to a Plugin
1. Add `domain` relation to all models without it
* This includes their `Content` models as base `Content` does not have it.
2. Update custom model serializers to include `domain` field and have correct uniqueness constraint
4. Ensure any custom serializer prevents cross-domain parameters
* `ValidateFieldsMixin` has a method for this
2. Update each task that uses objects to include the `domain` field.
* Basic syncs and publishes will probably need no changes.
3. Update each viewset that creates tasks to include the `domain` in the `dispatch` call.
6. Add the appropiate `has_domain_perms` checks to the viewset's AccessPolicies.
7. Update any extra URL routes to include `{domain_path}` if `DOMAIN_ENABLED`
8. Add `domain_compatiable = True` to `PluginAppConfig`
9. Add tests & documentation
---