CMIS authoring operations

This commit is contained in:
2026-05-07 01:46:44 +02:00
parent 7e168e93d3
commit e02f78d7e3
6 changed files with 282 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository
from kontextual_engine.core import (
Actor,
ActorType,
AssetRepresentation,
AuditEvent,
AuditOutcome,
Classification,
@@ -33,6 +34,7 @@ from kontextual_engine.core import (
PolicyDecision,
PolicyEffect,
RelationshipTargetKind,
RepresentationKind,
RetrievalFeedbackLabel,
SourceReference,
TransformationRunStatus,
@@ -419,6 +421,127 @@ class ServiceRuntime:
)
return content_stream
def cmis_create_document(
self,
access_point_id: str,
payload: dict[str, Any],
context: OperationContext,
) -> dict[str, Any]:
mapper = self._cmis_mapper(access_point_id)
decision = mapper.access_point.decide_action(CMISAction.CREATE_DOCUMENT, context)
if not decision.allowed:
raise _cmis_authorization_error(decision, "createDocument")
classification = Classification.from_dict(
{
"asset_type": payload.get("asset_type", "document"),
"sensitivity": payload.get("sensitivity", "internal"),
"owner": payload.get("owner"),
"topics": payload.get("topics", []),
"metadata": dict(payload.get("classification_metadata", {})),
}
)
content = payload.get("content")
representations = []
if content is not None:
representations.append(
AssetRepresentation.from_content(
payload.get("asset_id") or "cmis-new-document",
RepresentationKind.SOURCE,
payload.get("media_type", "text/plain"),
content,
storage_ref=payload.get("storage_ref"),
)
)
result = self.asset_service().create_asset(
payload["name"],
classification,
context,
asset_id=payload.get("asset_id"),
representations=representations,
metadata_records=[_metadata_record(item) for item in payload.get("metadata_records", [])],
idempotency_key=payload.get("idempotency_key"),
)
return self.cmis_object(access_point_id, mapper.asset_object_id(result.asset.id), context)
def cmis_update_properties(
self,
access_point_id: str,
object_id: str,
payload: dict[str, Any],
context: OperationContext,
) -> dict[str, Any]:
mapper = self._cmis_mapper(access_point_id)
decision = mapper.access_point.decide_action(CMISAction.UPDATE_PROPERTIES, context, resource=object_id)
if not decision.allowed:
raise _cmis_authorization_error(decision, "updateProperties")
asset_id = _cmis_asset_id(object_id)
properties = dict(payload.get("properties", payload))
expected = properties.pop("expected_current_version_id", payload.get("expected_current_version_id", None))
for key, value in properties.items():
if key.startswith("cmis:"):
continue
self.asset_service().add_metadata_record(
asset_id,
MetadataRecord(key=_cmis_metadata_key(key), value=value, confirmed=bool(payload.get("confirmed", True))),
context,
expected_current_version_id=expected,
)
expected = None
return self.cmis_object(access_point_id, object_id, context)
def cmis_set_content_stream(
self,
access_point_id: str,
object_id: str,
payload: dict[str, Any],
context: OperationContext,
) -> dict[str, Any]:
mapper = self._cmis_mapper(access_point_id)
decision = mapper.access_point.decide_action(CMISAction.SET_CONTENT_STREAM, context, resource=object_id)
if not decision.allowed:
raise _cmis_authorization_error(decision, "setContentStream")
asset_id = _cmis_asset_id(object_id)
representation = AssetRepresentation.from_content(
asset_id,
payload.get("kind", RepresentationKind.SOURCE.value),
payload.get("media_type", "text/plain"),
payload.get("content", ""),
storage_ref=payload.get("storage_ref"),
)
self.asset_service().add_representation(
asset_id,
representation,
context,
expected_current_version_id=payload.get("expected_current_version_id"),
)
return self.cmis_object(access_point_id, object_id, context)
def cmis_delete_object(
self,
access_point_id: str,
object_id: str,
payload: dict[str, Any],
context: OperationContext,
) -> dict[str, Any]:
mapper = self._cmis_mapper(access_point_id)
decision = mapper.access_point.decide_action(CMISAction.DELETE_OBJECT, context, resource=object_id)
if not decision.allowed:
raise _cmis_authorization_error(decision, "deleteObject")
asset_id = _cmis_asset_id(object_id)
result = self.asset_service().request_delete(
asset_id,
context,
expected_current_version_id=payload.get("expected_current_version_id"),
)
return {
"object_id": mapper.asset_object_id(asset_id),
"deleted": False,
"lifecycle": result.asset.lifecycle.value,
"version": result.version.to_dict(),
"audit_event": result.audit_event.to_dict(),
"policy_decision": result.policy_decision.to_dict(),
}
def cmis_query(
self,
access_point_id: str,
@@ -1964,6 +2087,41 @@ def create_app(runtime: ServiceRuntime | None = None):
) -> dict[str, Any]:
return response(runtime.cmis_content_stream, access_point_id, object_id, context)
@app.post("/cmis/{access_point_id}/browser/document", tags=["cmis"])
def cmis_create_document(
access_point_id: str,
payload: dict[str, Any],
context: OperationContext = Depends(context_from_headers),
) -> dict[str, Any]:
return response(runtime.cmis_create_document, access_point_id, payload, context)
@app.post("/cmis/{access_point_id}/browser/object/{object_id:path}/properties", tags=["cmis"])
def cmis_update_properties(
access_point_id: str,
object_id: str,
payload: dict[str, Any],
context: OperationContext = Depends(context_from_headers),
) -> dict[str, Any]:
return response(runtime.cmis_update_properties, access_point_id, object_id, payload, context)
@app.post("/cmis/{access_point_id}/browser/object/{object_id:path}/content", tags=["cmis"])
def cmis_set_content_stream(
access_point_id: str,
object_id: str,
payload: dict[str, Any],
context: OperationContext = Depends(context_from_headers),
) -> dict[str, Any]:
return response(runtime.cmis_set_content_stream, access_point_id, object_id, payload, context)
@app.post("/cmis/{access_point_id}/browser/object/{object_id:path}/delete", tags=["cmis"])
def cmis_delete_object(
access_point_id: str,
object_id: str,
payload: dict[str, Any] | None = None,
context: OperationContext = Depends(context_from_headers),
) -> dict[str, Any]:
return response(runtime.cmis_delete_object, access_point_id, object_id, payload or {}, context)
@app.get("/cmis/{access_point_id}/browser/query", tags=["cmis"])
def cmis_query(
access_point_id: str,
@@ -2442,6 +2600,14 @@ def _cmis_change_type(operation: str) -> str:
return "security"
def _cmis_metadata_key(key: str) -> str:
if key.startswith("kontextual:metadata:"):
return key.removeprefix("kontextual:metadata:")
if key.startswith("kontextual:"):
return key.removeprefix("kontextual:")
return key
def _age_seconds(start: str, end: str) -> float:
try:
start_dt = datetime.fromisoformat(start.replace("Z", "+00:00"))