generated from coulomb/repo-seed
CMIS layer into an honest CMIS 1.1
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
||||
|
||||
from .actors import ActorType, OperationContext
|
||||
from .assets import AssetRepresentation, KnowledgeAsset, RepresentationKind
|
||||
from .metadata import Sensitivity
|
||||
from .metadata import LifecycleState, Sensitivity
|
||||
from .policy import PolicyDecision
|
||||
from .provenance import AssetVersion
|
||||
from .relationships import CoreRelationship, RelationshipTargetKind
|
||||
@@ -92,6 +92,24 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
||||
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
|
||||
CMISAction.BULK_UPDATE_PROPERTIES: CMISCapability.BULK_UPDATE,
|
||||
}
|
||||
IMPLEMENTED_CMIS_ACTIONS: frozenset[CMISAction] = frozenset(
|
||||
{
|
||||
CMISAction.GET_REPOSITORY_INFO,
|
||||
CMISAction.GET_TYPE_DEFINITION,
|
||||
CMISAction.GET_CHILDREN,
|
||||
CMISAction.GET_OBJECT_PARENTS,
|
||||
CMISAction.GET_OBJECT,
|
||||
CMISAction.GET_CONTENT_STREAM,
|
||||
CMISAction.GET_ACL,
|
||||
CMISAction.QUERY,
|
||||
CMISAction.GET_RELATIONSHIPS,
|
||||
CMISAction.GET_CHANGE_LOG,
|
||||
CMISAction.CREATE_DOCUMENT,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
}
|
||||
)
|
||||
MUTATION_ACTIONS = {
|
||||
CMISAction.CREATE_DOCUMENT,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
@@ -101,6 +119,90 @@ MUTATION_ACTIONS = {
|
||||
CMISAction.APPLY_POLICY,
|
||||
CMISAction.BULK_UPDATE_PROPERTIES,
|
||||
}
|
||||
NEW_TYPE_SETTABLE_ATTRIBUTES: dict[str, bool] = {
|
||||
"id": False,
|
||||
"local_name": False,
|
||||
"local_namespace": False,
|
||||
"display_name": False,
|
||||
"query_name": False,
|
||||
"description": False,
|
||||
"creatable": False,
|
||||
"fileable": False,
|
||||
"queryable": False,
|
||||
"fulltext_indexed": False,
|
||||
"included_in_supertype_query": False,
|
||||
"controllable_policy": False,
|
||||
"controllable_acl": False,
|
||||
}
|
||||
UNSUPPORTED_FEATURES: dict[str, dict[str, Any]] = {
|
||||
"atompub": {"status": "unsupported", "reason": "binding_not_supported", "intent": "deferred"},
|
||||
"web_services": {"status": "unsupported", "reason": "binding_not_supported", "intent": "deferred"},
|
||||
"get_descendants": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_get_descendants",
|
||||
},
|
||||
"get_folder_tree": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_get_folder_tree",
|
||||
},
|
||||
"multifiling": {
|
||||
"status": "projection_only",
|
||||
"reason": "mutation_capability_not_supported",
|
||||
"standard_flag": "capability_multifiling",
|
||||
},
|
||||
"unfiling": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_unfiling",
|
||||
},
|
||||
"versioning_services": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "document_type.versionable",
|
||||
},
|
||||
"private_working_copy": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flags": ["capability_pwc_searchable", "capability_pwc_updatable"],
|
||||
},
|
||||
"all_versions_search": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_all_versions_searchable",
|
||||
},
|
||||
"full_cmis_sql_joins": {
|
||||
"status": "unsupported",
|
||||
"reason": "query_not_supported",
|
||||
"standard_flag": "capability_join",
|
||||
},
|
||||
"order_by": {"status": "unsupported", "reason": "query_not_supported", "standard_flag": "capability_order_by"},
|
||||
"append_content_stream": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_content_stream_updatability",
|
||||
},
|
||||
"apply_acl": {"status": "unsupported", "reason": "operation_not_implemented", "standard_flag": "capability_acl"},
|
||||
"apply_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
||||
"remove_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
||||
"retention_hold_mutation": {"status": "unsupported", "reason": "capability_not_supported"},
|
||||
"bulk_update_properties": {
|
||||
"status": "unsupported",
|
||||
"reason": "operation_not_implemented",
|
||||
"standard_service": "bulkUpdateProperties",
|
||||
},
|
||||
"rendition_streams": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_renditions",
|
||||
},
|
||||
"type_mutability": {
|
||||
"status": "unsupported",
|
||||
"reason": "capability_not_supported",
|
||||
"standard_flag": "capability_new_type_settable_attributes",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -170,11 +272,7 @@ class CMISAccessProfile:
|
||||
def admin_export(cls) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name="admin-export",
|
||||
capabilities=_read_capabilities()
|
||||
+ (
|
||||
CMISCapability.RENDITIONS,
|
||||
CMISCapability.RETENTION_HOLD,
|
||||
),
|
||||
capabilities=_read_capabilities(),
|
||||
allow_mutations=False,
|
||||
visible_sensitivities=(
|
||||
Sensitivity.PUBLIC,
|
||||
@@ -203,13 +301,28 @@ class CMISAccessProfile:
|
||||
return CMISCapability(capability) in self.capabilities
|
||||
|
||||
def allows_action(self, action: CMISAction | str) -> bool:
|
||||
cmis_action = CMISAction(action)
|
||||
return self.action_denial(cmis_action) is None
|
||||
|
||||
def action_denial(self, action: CMISAction | str) -> tuple[str, dict[str, Any]] | None:
|
||||
cmis_action = CMISAction(action)
|
||||
capability = ACTION_CAPABILITIES[cmis_action]
|
||||
if not self.has_capability(capability):
|
||||
return False
|
||||
if cmis_action in MUTATION_ACTIONS and not self.allow_mutations:
|
||||
return False
|
||||
return True
|
||||
return (
|
||||
"cmis_mutation_not_allowed",
|
||||
{"profile": self.name, "capability": capability.value, "mutation": cmis_action.value},
|
||||
)
|
||||
if not self.has_capability(capability):
|
||||
return (
|
||||
"cmis_capability_not_supported",
|
||||
{"profile": self.name, "capability": capability.value},
|
||||
)
|
||||
if cmis_action not in IMPLEMENTED_CMIS_ACTIONS:
|
||||
return (
|
||||
"cmis_operation_not_implemented",
|
||||
{"profile": self.name, "capability": capability.value, "operation": cmis_action.value},
|
||||
)
|
||||
return None
|
||||
|
||||
def decide_action(
|
||||
self,
|
||||
@@ -234,14 +347,16 @@ class CMISAccessProfile:
|
||||
resource,
|
||||
context={"profile": self.name},
|
||||
)
|
||||
capability = ACTION_CAPABILITIES[cmis_action]
|
||||
reason = "cmis_mutation_not_allowed" if cmis_action in MUTATION_ACTIONS else "cmis_capability_not_supported"
|
||||
reason, denial_context = self.action_denial(cmis_action) or (
|
||||
"cmis_capability_not_supported",
|
||||
{"profile": self.name},
|
||||
)
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
cmis_action.value,
|
||||
resource,
|
||||
reason=reason,
|
||||
context={"profile": self.name, "capability": capability.value},
|
||||
context=denial_context,
|
||||
)
|
||||
|
||||
def allows_actor(self, context: OperationContext) -> bool:
|
||||
@@ -265,6 +380,14 @@ class CMISAccessProfile:
|
||||
context={"profile": self.name},
|
||||
)
|
||||
classification = asset.classification
|
||||
if asset.lifecycle == LifecycleState.DELETE_REQUESTED:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_lifecycle_not_visible",
|
||||
context={"profile": self.name, "lifecycle": _enum_value(asset.lifecycle)},
|
||||
)
|
||||
if classification.sensitivity in self.denied_sensitivities:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
@@ -466,13 +589,27 @@ class CMISDomainMapper:
|
||||
return {
|
||||
"repository_id": self.access_point.repository_id,
|
||||
"repository_name": self.access_point.metadata.get("repository_name", self.access_point.repository_id),
|
||||
"repository_description": self.access_point.metadata.get(
|
||||
"repository_description",
|
||||
"Kontextual Engine CMIS Browser Binding access point",
|
||||
),
|
||||
"cmis_version_supported": "1.1",
|
||||
"root_folder_id": self.access_point.root_folder_id,
|
||||
"principal_anonymous": "anonymous",
|
||||
"principal_anyone": "anyone",
|
||||
"vendor_name": "Kontextual",
|
||||
"product_name": "kontextual-engine",
|
||||
"product_version": self.access_point.metadata.get("product_version", "0.1.0"),
|
||||
"binding": profile.binding.value,
|
||||
"capabilities": self.capability_flags(),
|
||||
"repository_features": self.repository_features(),
|
||||
"unsupported_features": self.unsupported_features(),
|
||||
"compliance": {
|
||||
"standard": "CMIS 1.1",
|
||||
"binding": profile.binding.value,
|
||||
"posture": "declared-browser-binding-subset",
|
||||
"overclaim_policy": "unsupported_optional_capabilities_are_advertised_as false/none",
|
||||
},
|
||||
"profile": profile.name,
|
||||
}
|
||||
|
||||
@@ -480,27 +617,53 @@ class CMISDomainMapper:
|
||||
profile = self.access_point.profile
|
||||
return {
|
||||
"capability_content_stream_updatability": (
|
||||
"anytime" if profile.has_capability(CMISCapability.CONTENT_STREAM_WRITE) else "none"
|
||||
"anytime"
|
||||
if profile.allow_mutations and profile.has_capability(CMISCapability.CONTENT_STREAM_WRITE)
|
||||
else "none"
|
||||
),
|
||||
"capability_changes": "objectidsonly"
|
||||
if profile.has_capability(CMISCapability.CHANGE_LOG)
|
||||
else "none",
|
||||
"capability_renditions": "read" if profile.has_capability(CMISCapability.RENDITIONS) else "none",
|
||||
"capability_get_descendants": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_get_folder_tree": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_multifiling": profile.has_capability(CMISCapability.NAVIGATION),
|
||||
"capability_renditions": "none",
|
||||
"capability_get_descendants": False,
|
||||
"capability_get_folder_tree": False,
|
||||
"capability_order_by": "none",
|
||||
"capability_multifiling": False,
|
||||
"capability_unfiling": False,
|
||||
"capability_version_specific_filing": False,
|
||||
"capability_pwc_searchable": False,
|
||||
"capability_pwc_updatable": False,
|
||||
"capability_all_versions_searchable": profile.has_capability(CMISCapability.VERSIONING),
|
||||
"capability_all_versions_searchable": False,
|
||||
"capability_query": "metadataonly"
|
||||
if profile.has_capability(CMISCapability.DISCOVERY_QUERY)
|
||||
else "none",
|
||||
"capability_join": "none",
|
||||
"capability_acl": "discover" if profile.has_capability(CMISCapability.ACL) else "none",
|
||||
"capability_new_type_settable_attributes": dict(NEW_TYPE_SETTABLE_ATTRIBUTES),
|
||||
}
|
||||
|
||||
def repository_features(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"id": "urn:kontextual:cmis:feature:profiled-access-points",
|
||||
"common_name": "Profiled CMIS access points",
|
||||
"version_label": "1.0",
|
||||
"description": "Multiple Browser Binding access points can expose different governed profile slices.",
|
||||
},
|
||||
{
|
||||
"id": "urn:kontextual:cmis:feature:projection-parentage",
|
||||
"common_name": "Projection-only parent folder maps",
|
||||
"version_label": "1.0",
|
||||
"description": (
|
||||
"Assets may appear under multiple virtual folder projections; CMIS multi-filing mutation "
|
||||
"services are not advertised."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
def unsupported_features(self) -> dict[str, dict[str, Any]]:
|
||||
return {key: dict(value) for key, value in UNSUPPORTED_FEATURES.items()}
|
||||
|
||||
def type_definitions(self) -> list[dict[str, Any]]:
|
||||
can_write = self.access_point.profile.allow_mutations
|
||||
return [
|
||||
@@ -537,7 +700,7 @@ class CMISDomainMapper:
|
||||
type_id=f"kontextual:{asset.classification.asset_type}",
|
||||
name=asset.title,
|
||||
path=self.asset_path(asset),
|
||||
properties=self.asset_properties(asset, metadata_records=metadata_records),
|
||||
properties=self.asset_properties(asset, metadata_records=metadata_records, content_stream=content_stream),
|
||||
allowable_actions=self.allowable_actions(context, has_content_stream=content_stream is not None),
|
||||
content_stream=content_stream,
|
||||
version=self.version_properties(asset, current_version, versions),
|
||||
@@ -682,6 +845,7 @@ class CMISDomainMapper:
|
||||
asset: KnowledgeAsset,
|
||||
*,
|
||||
metadata_records: list[Any] | tuple[Any, ...] = (),
|
||||
content_stream: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
classification = asset.classification
|
||||
properties = {
|
||||
@@ -694,6 +858,10 @@ class CMISDomainMapper:
|
||||
"cmis:creationDate": asset.created_at,
|
||||
"cmis:lastModificationDate": asset.updated_at,
|
||||
"cmis:changeToken": asset.current_version_id,
|
||||
"cmis:contentStreamLength": content_stream.get("length") if content_stream else None,
|
||||
"cmis:contentStreamMimeType": content_stream.get("mime_type") if content_stream else None,
|
||||
"cmis:contentStreamFileName": content_stream.get("file_name") if content_stream else None,
|
||||
"cmis:contentStreamId": content_stream.get("stream_id") if content_stream else None,
|
||||
"kontextual:assetId": asset.id,
|
||||
"kontextual:assetType": classification.asset_type,
|
||||
"kontextual:sensitivity": _enum_value(classification.sensitivity),
|
||||
@@ -785,7 +953,6 @@ def _read_capabilities() -> tuple[CMISCapability, ...]:
|
||||
CMISCapability.NAVIGATION,
|
||||
CMISCapability.OBJECT_READ,
|
||||
CMISCapability.CONTENT_STREAM_READ,
|
||||
CMISCapability.VERSIONING,
|
||||
CMISCapability.DISCOVERY_QUERY,
|
||||
CMISCapability.RELATIONSHIPS,
|
||||
CMISCapability.ACL,
|
||||
@@ -803,19 +970,20 @@ def _type_definition(
|
||||
display_name: str,
|
||||
can_write: bool,
|
||||
) -> dict[str, Any]:
|
||||
is_document = base_type_id == CMISBaseType.DOCUMENT
|
||||
return {
|
||||
"id": type_id,
|
||||
"local_name": type_id.split(":", 1)[-1],
|
||||
"display_name": display_name,
|
||||
"base_type_id": base_type_id.value,
|
||||
"queryable": True,
|
||||
"controllable_acl": base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
|
||||
"queryable": is_document,
|
||||
"controllable_acl": is_document,
|
||||
"controllable_policy": False,
|
||||
"creatable": can_write and base_type_id == CMISBaseType.DOCUMENT,
|
||||
"fileable": base_type_id == CMISBaseType.DOCUMENT,
|
||||
"creatable": can_write and is_document,
|
||||
"fileable": is_document,
|
||||
"fulltext_indexed": False,
|
||||
"included_in_supertype_query": True,
|
||||
"versionable": base_type_id == CMISBaseType.DOCUMENT,
|
||||
"included_in_supertype_query": is_document,
|
||||
"versionable": False,
|
||||
"property_definitions": _property_definitions(base_type_id),
|
||||
}
|
||||
|
||||
@@ -829,6 +997,31 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
|
||||
"kontextual:sensitivity": {"property_type": "string", "cardinality": "single", "required": False},
|
||||
"kontextual:lifecycle": {"property_type": "string", "cardinality": "single", "required": False},
|
||||
}
|
||||
if base_type_id == CMISBaseType.DOCUMENT:
|
||||
definitions.update(
|
||||
{
|
||||
"cmis:contentStreamLength": {
|
||||
"property_type": "integer",
|
||||
"cardinality": "single",
|
||||
"required": False,
|
||||
},
|
||||
"cmis:contentStreamMimeType": {
|
||||
"property_type": "string",
|
||||
"cardinality": "single",
|
||||
"required": False,
|
||||
},
|
||||
"cmis:contentStreamFileName": {
|
||||
"property_type": "string",
|
||||
"cardinality": "single",
|
||||
"required": False,
|
||||
},
|
||||
"cmis:contentStreamId": {
|
||||
"property_type": "id",
|
||||
"cardinality": "single",
|
||||
"required": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
if base_type_id == CMISBaseType.RELATIONSHIP:
|
||||
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||
|
||||
Reference in New Issue
Block a user