Add multi-application catalog support
This commit is contained in:
parent
6d0cc7b0f9
commit
1440a597df
7 changed files with 525 additions and 13 deletions
|
|
@ -585,6 +585,8 @@ class UserEngineService:
|
|||
tenant: str | None = None,
|
||||
correlation_id: str | None = None,
|
||||
) -> Projection:
|
||||
if _is_application_projection(projection_type) and application_id is None:
|
||||
raise ValidationError("application projections require application_id")
|
||||
tenant_context = self.resolve_tenant_context(actor, tenant)
|
||||
correlation_id = correlation_id or new_id("corr")
|
||||
self._authorize(
|
||||
|
|
@ -601,9 +603,14 @@ class UserEngineService:
|
|||
effective = self._resolve_effective_profile(
|
||||
user_id, application_id, tenant_context.tenant
|
||||
)
|
||||
owning_application_id = (
|
||||
application_id if _is_application_projection(projection_type) else None
|
||||
)
|
||||
values: dict[str, Any] = {}
|
||||
redactions: dict[str, str] = {}
|
||||
for key, definition in self._active_attribute_definitions().items():
|
||||
for key, definition in self._active_attribute_definitions(
|
||||
owning_application_id=owning_application_id
|
||||
).items():
|
||||
if key not in effective.values:
|
||||
continue
|
||||
if not _visible_in_projection(definition, projection_type):
|
||||
|
|
@ -863,9 +870,26 @@ class UserEngineService:
|
|||
if binding and catalog.namespace not in binding.catalog_namespaces:
|
||||
raise ValidationError("catalog namespace is not bound to application")
|
||||
|
||||
existing_catalog = self.store.catalogs.get(catalog.catalog_id)
|
||||
if existing_catalog is not None:
|
||||
self._validate_catalog_update(existing_catalog, catalog)
|
||||
|
||||
active_definitions = self._active_attribute_definitions(
|
||||
excluding_catalog_id=catalog.catalog_id
|
||||
)
|
||||
active_catalogs = (
|
||||
existing
|
||||
for existing in self.store.catalogs.values()
|
||||
if existing.catalog_id != catalog.catalog_id
|
||||
and existing.lifecycle == CatalogLifecycle.ACTIVE
|
||||
)
|
||||
for existing in active_catalogs:
|
||||
if (
|
||||
existing.namespace == catalog.namespace
|
||||
and existing.owning_application_id != catalog.owning_application_id
|
||||
):
|
||||
raise ConflictError("active catalog namespace is already owned")
|
||||
|
||||
for attribute in catalog.attributes:
|
||||
if not attribute.key.startswith(f"{catalog.namespace}."):
|
||||
raise ValidationError("attribute keys must use the catalog namespace")
|
||||
|
|
@ -880,6 +904,26 @@ class UserEngineService:
|
|||
if attribute.default is not None:
|
||||
self._validate_value(attribute, attribute.default)
|
||||
|
||||
def _validate_catalog_update(self, previous: Catalog, catalog: Catalog) -> None:
|
||||
if previous.namespace != catalog.namespace:
|
||||
raise ValidationError("catalog namespace cannot change")
|
||||
if previous.owning_application_id != catalog.owning_application_id:
|
||||
raise ValidationError("catalog owner cannot change")
|
||||
if _version_tuple(catalog.version) < _version_tuple(previous.version):
|
||||
raise ValidationError("catalog version cannot move backwards")
|
||||
|
||||
previous_attributes = {
|
||||
attribute.key: attribute for attribute in previous.attributes
|
||||
}
|
||||
for attribute in catalog.attributes:
|
||||
previous_attribute = previous_attributes.get(attribute.key)
|
||||
if previous_attribute is None:
|
||||
continue
|
||||
if _sensitivity_rank(attribute.sensitivity) < _sensitivity_rank(
|
||||
previous_attribute.sensitivity
|
||||
):
|
||||
raise ValidationError("catalog update cannot downgrade sensitivity")
|
||||
|
||||
def _validate_profile_scope(
|
||||
self,
|
||||
definition: AttributeDefinition,
|
||||
|
|
@ -947,12 +991,20 @@ class UserEngineService:
|
|||
return definition
|
||||
|
||||
def _active_attribute_definitions(
|
||||
self, *, excluding_catalog_id: str | None = None
|
||||
self,
|
||||
*,
|
||||
excluding_catalog_id: str | None = None,
|
||||
owning_application_id: str | None = None,
|
||||
) -> dict[str, AttributeDefinition]:
|
||||
definitions: dict[str, AttributeDefinition] = {}
|
||||
for catalog in self.store.catalogs.values():
|
||||
if catalog.catalog_id == excluding_catalog_id:
|
||||
continue
|
||||
if (
|
||||
owning_application_id is not None
|
||||
and catalog.owning_application_id != owning_application_id
|
||||
):
|
||||
continue
|
||||
if catalog.lifecycle != CatalogLifecycle.ACTIVE:
|
||||
continue
|
||||
for attribute in catalog.attributes:
|
||||
|
|
@ -984,6 +1036,14 @@ def _visible_in_projection(
|
|||
return visibility in definition.visibility
|
||||
|
||||
|
||||
def _is_application_projection(projection_type: ProjectionType) -> bool:
|
||||
return projection_type in {
|
||||
ProjectionType.APPLICATION_RUNTIME,
|
||||
ProjectionType.AGENT_CONTEXT,
|
||||
ProjectionType.CLAIMS_ENRICHMENT,
|
||||
}
|
||||
|
||||
|
||||
def _must_redact(
|
||||
definition: AttributeDefinition, projection_type: ProjectionType
|
||||
) -> bool:
|
||||
|
|
@ -994,3 +1054,20 @@ def _must_redact(
|
|||
}:
|
||||
return False
|
||||
return definition.sensitivity in {Sensitivity.SENSITIVE, Sensitivity.SECRET}
|
||||
|
||||
|
||||
def _version_tuple(version: str) -> tuple[int, int, int]:
|
||||
parts = version.split(".")
|
||||
if len(parts) != 3 or any(not part.isdigit() for part in parts):
|
||||
raise ValidationError("catalog version must be semantic MAJOR.MINOR.PATCH")
|
||||
return tuple(int(part) for part in parts)
|
||||
|
||||
|
||||
def _sensitivity_rank(sensitivity: Sensitivity) -> int:
|
||||
return {
|
||||
Sensitivity.PUBLIC: 0,
|
||||
Sensitivity.INTERNAL: 1,
|
||||
Sensitivity.PERSONAL: 2,
|
||||
Sensitivity.SENSITIVE: 3,
|
||||
Sensitivity.SECRET: 4,
|
||||
}[sensitivity]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue