generated from coulomb/repo-seed
CMIS compliance/test foundation
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Date: 2026-05-06
|
||||
|
||||
Status: planned test foundation for CMIS access-point work.
|
||||
Status: initial test foundation established for CMIS access-point work.
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -17,9 +17,9 @@ harness.
|
||||
|
||||
Planned harness shape:
|
||||
|
||||
- `tests/cmis/examples/` contains deterministic fixture descriptions grouped by
|
||||
CMIS service capability.
|
||||
- `tests/cmis/test_cmis_contract_examples.py` validates mapper and profile
|
||||
- `examples/cmis/` contains deterministic fixture descriptions grouped by CMIS
|
||||
service capability.
|
||||
- `tests/cmis/test_cmis_contract_examples.py` validates fixture and profile
|
||||
behavior without external Java dependencies.
|
||||
- `tests/cmis/opencmis-tck/` contains optional harness config, Maven
|
||||
invocation notes, and selected TCK group mapping.
|
||||
@@ -188,3 +188,10 @@ Validates:
|
||||
No current OASIS certification service was identified during planning. The
|
||||
practical reusable foundation is OpenCMIS TCK/Workbench, plus our own
|
||||
capability-profile contract tests.
|
||||
|
||||
## Established Artifacts
|
||||
|
||||
- `examples/cmis/capability-fixtures.json`
|
||||
- `tests/cmis/test_cmis_contract_examples.py`
|
||||
- `tests/cmis/opencmis-tck/README.md`
|
||||
- `docs/cmis-readiness-gate.md`
|
||||
|
||||
48
docs/cmis-profiled-access-points-implementation.md
Normal file
48
docs/cmis-profiled-access-points-implementation.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# CMIS Profiled Access Points Implementation
|
||||
|
||||
Date: 2026-05-06
|
||||
|
||||
Status: first implementation slice started.
|
||||
|
||||
## Implemented Slice
|
||||
|
||||
`src/kontextual_engine/core/cmis.py` defines the CMIS profile and access-point
|
||||
boundary used by the future API adapter:
|
||||
|
||||
- `CMISBinding`
|
||||
- `CMISCapability`
|
||||
- `CMISAction`
|
||||
- `CMISAccessProfile`
|
||||
- `CMISAccessPoint`
|
||||
|
||||
The layer is intentionally small. It decides whether a CMIS action is allowed
|
||||
for a profile and whether an engine asset may be exposed through an access
|
||||
point. It does not implement CMIS routes and does not duplicate asset storage,
|
||||
metadata, relationship, policy, or audit services.
|
||||
|
||||
## Built-In Profiles
|
||||
|
||||
- `readonly-browser`: Browser Binding read profile over public/internal assets.
|
||||
- `governed-authoring`: Browser Binding profile with selected create/update
|
||||
and content stream mutations.
|
||||
- `admin-export`: service-account-only export profile with broad visibility.
|
||||
- `compat-tck`: Browser Binding profile intended for selected OpenCMIS TCK
|
||||
compatibility tests.
|
||||
|
||||
## Enforcement Boundary
|
||||
|
||||
Profiles can restrict exposure by:
|
||||
|
||||
- CMIS capability,
|
||||
- mutation allowance,
|
||||
- actor type,
|
||||
- sensitivity,
|
||||
- asset type,
|
||||
- topic,
|
||||
- source system,
|
||||
- metadata deny rules.
|
||||
|
||||
Decisions return existing `PolicyDecision` objects so later CMIS routes can
|
||||
emit compatible diagnostics and audit records without inventing another policy
|
||||
model.
|
||||
|
||||
37
docs/cmis-readiness-gate.md
Normal file
37
docs/cmis-readiness-gate.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# CMIS Implementation Readiness Gate
|
||||
|
||||
Date: 2026-05-06
|
||||
|
||||
Status: ready for `KONT-WP-0012` implementation planning.
|
||||
|
||||
## Required Before Implementation
|
||||
|
||||
- CMIS target version is fixed at OASIS CMIS 1.1.
|
||||
- Browser Binding is the first implementation target.
|
||||
- AtomPub and Web Services bindings are explicitly deferred.
|
||||
- Capability examples are grouped in `examples/cmis/capability-fixtures.json`.
|
||||
- Internal fixture contract tests validate profile expectations.
|
||||
- OpenCMIS TCK is documented as an optional external harness.
|
||||
- The first implementation profile is constrained to profile-scoped Browser
|
||||
Binding behavior rather than full CMIS certification claims.
|
||||
|
||||
## First Implementation Slice
|
||||
|
||||
Implement the profile and mapper layer before routes:
|
||||
|
||||
1. Profile model and access-point configuration.
|
||||
2. CMIS object/type/capability projection over existing engine services.
|
||||
3. Profile-scoped visibility denial and mutation policy.
|
||||
4. Browser Binding read routes.
|
||||
5. Governed mutation routes.
|
||||
6. Optional OpenCMIS TCK compatibility profile.
|
||||
|
||||
## Non-Goals For The First Slice
|
||||
|
||||
- Full AtomPub support.
|
||||
- Full SOAP/Web Services support.
|
||||
- Full CMIS SQL grammar.
|
||||
- Full private-working-copy versioning semantics.
|
||||
- Legal hold or retention mutation semantics.
|
||||
- General-purpose CMIS certification claim.
|
||||
|
||||
15
examples/cmis/README.md
Normal file
15
examples/cmis/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# CMIS Example Fixtures
|
||||
|
||||
These fixtures define the expected CMIS behavior before the CMIS adapter is
|
||||
implemented. They are grouped by CMIS capability so internal contract tests and
|
||||
optional OpenCMIS TCK runs can validate profile behavior consistently.
|
||||
|
||||
The fixtures are not a second domain model. They describe how existing
|
||||
`kontextual-engine` assets, relationships, versions, policy decisions, audit
|
||||
events, and representations should be projected through CMIS access points.
|
||||
|
||||
Files:
|
||||
|
||||
- `capability-fixtures.json`: capability groups, example object inventory,
|
||||
profile expectations, and unsupported-operation diagnostics.
|
||||
|
||||
203
examples/cmis/capability-fixtures.json
Normal file
203
examples/cmis/capability-fixtures.json
Normal file
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"standard": {
|
||||
"name": "CMIS",
|
||||
"version": "1.1",
|
||||
"primary_binding": "browser",
|
||||
"deferred_bindings": ["atompub", "web_services"]
|
||||
},
|
||||
"profiles": {
|
||||
"readonly-browser": {
|
||||
"description": "Read-only Browser Binding access point for governed content access.",
|
||||
"binding": "browser",
|
||||
"mutations": false,
|
||||
"visibility": ["public", "internal"],
|
||||
"deny": ["confidential"],
|
||||
"expected_tck_posture": "repository/type/read/navigation/query/content-read subset"
|
||||
},
|
||||
"governed-authoring": {
|
||||
"description": "Browser Binding access point with selected governed mutations.",
|
||||
"binding": "browser",
|
||||
"mutations": true,
|
||||
"visibility": ["public", "internal"],
|
||||
"deny": ["confidential"],
|
||||
"expected_tck_posture": "readonly subset plus selected create/update/delete checks"
|
||||
},
|
||||
"admin-export": {
|
||||
"description": "Service-account export profile for broad governance inspection.",
|
||||
"binding": "browser",
|
||||
"mutations": false,
|
||||
"visibility": ["public", "internal", "confidential"],
|
||||
"deny": [],
|
||||
"expected_tck_posture": "internal contract tests, not general client compatibility"
|
||||
},
|
||||
"compat-tck": {
|
||||
"description": "Compatibility profile tuned for selected OpenCMIS TCK groups.",
|
||||
"binding": "browser",
|
||||
"mutations": true,
|
||||
"visibility": ["public", "internal"],
|
||||
"deny": ["confidential"],
|
||||
"expected_tck_posture": "selected OpenCMIS TCK Browser Binding subset"
|
||||
}
|
||||
},
|
||||
"objects": [
|
||||
{
|
||||
"id": "doc-public-source",
|
||||
"kind": "document",
|
||||
"title": "Public source document",
|
||||
"sensitivity": "public",
|
||||
"content_stream": true,
|
||||
"media_type": "text/markdown",
|
||||
"version_series": "vs-public-source",
|
||||
"version_label": "1.0",
|
||||
"folder_path": "/sources/public"
|
||||
},
|
||||
{
|
||||
"id": "doc-internal-normalized",
|
||||
"kind": "document",
|
||||
"title": "Internal normalized representation",
|
||||
"sensitivity": "internal",
|
||||
"content_stream": true,
|
||||
"media_type": "application/json",
|
||||
"version_series": "vs-internal-normalized",
|
||||
"version_label": "2.0",
|
||||
"folder_path": "/representations/normalized"
|
||||
},
|
||||
{
|
||||
"id": "doc-confidential-risk",
|
||||
"kind": "document",
|
||||
"title": "Confidential risk note",
|
||||
"sensitivity": "confidential",
|
||||
"content_stream": true,
|
||||
"media_type": "text/markdown",
|
||||
"version_series": "vs-confidential-risk",
|
||||
"version_label": "1.0",
|
||||
"folder_path": "/sources/confidential"
|
||||
},
|
||||
{
|
||||
"id": "rel-derived-from",
|
||||
"kind": "relationship",
|
||||
"source_id": "doc-internal-normalized",
|
||||
"target_id": "doc-public-source",
|
||||
"relationship_type": "derived_from"
|
||||
},
|
||||
{
|
||||
"id": "event-content-updated",
|
||||
"kind": "change_event",
|
||||
"change_type": "updated",
|
||||
"object_id": "doc-internal-normalized",
|
||||
"change_token": "chg-0002"
|
||||
}
|
||||
],
|
||||
"capability_groups": [
|
||||
{
|
||||
"id": "repository-type",
|
||||
"service": "repository",
|
||||
"examples": ["repo-browser", "readonly-browser", "governed-authoring"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["repository_info", "capability_flags", "base_type_definitions", "property_definitions"],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "navigation",
|
||||
"service": "navigation",
|
||||
"examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["get_children", "get_descendants", "get_folder_tree", "get_object_by_path"],
|
||||
"unsupported": ["multifiling", "unfiling"]
|
||||
},
|
||||
{
|
||||
"id": "object-content",
|
||||
"service": "object",
|
||||
"examples": ["source-document", "normalized-representation", "metadata-rich-document", "streamless-document"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["get_object", "get_properties", "get_content_stream", "allowable_actions"],
|
||||
"unsupported": ["append_content_stream"]
|
||||
},
|
||||
{
|
||||
"id": "versioning",
|
||||
"service": "versioning",
|
||||
"examples": ["single-version-document", "multi-version-document", "read-only-checkout"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["version_series", "latest_version_flags", "version_history"],
|
||||
"unsupported": ["private_working_copy"]
|
||||
},
|
||||
{
|
||||
"id": "discovery-query",
|
||||
"service": "discovery",
|
||||
"examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "unsupported-join"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["query_capabilities", "paging", "unsupported_grammar_diagnostics"],
|
||||
"unsupported": ["full_cmis_sql_joins"]
|
||||
},
|
||||
{
|
||||
"id": "relationships",
|
||||
"service": "relationship",
|
||||
"examples": ["asset-to-asset", "asset-to-context-entity", "lineage-relation"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["relationship_projection", "source_filter", "target_filter", "profile_visibility"],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "acl-policy",
|
||||
"service": "acl",
|
||||
"examples": ["public-asset", "internal-asset", "confidential-asset", "denied-actor", "service-account"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["allowable_actions", "acl_projection", "denial_omits_object", "no_metadata_leakage"],
|
||||
"unsupported": ["apply_policy", "remove_policy"]
|
||||
},
|
||||
{
|
||||
"id": "change-log",
|
||||
"service": "change_log",
|
||||
"examples": ["asset-create", "metadata-update", "content-update", "transformation-output", "workflow-run", "policy-denial"],
|
||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||
"must_validate": ["change_tokens", "event_ordering", "audit_correlation", "paging"],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "retention-renditions-bulk",
|
||||
"service": "extension",
|
||||
"examples": ["retention-metadata", "legal-hold-metadata", "preview-representation", "bulk-metadata-update"],
|
||||
"supported_profiles": ["admin-export"],
|
||||
"must_validate": ["explicit_capability_flags", "structured_unsupported_operation"],
|
||||
"unsupported": ["retention_hold_mutation", "bulk_update_properties", "rendition_streams"]
|
||||
}
|
||||
],
|
||||
"unsupported_diagnostics": {
|
||||
"atompub": "binding_not_supported",
|
||||
"web_services": "binding_not_supported",
|
||||
"multifiling": "capability_not_supported",
|
||||
"unfiling": "capability_not_supported",
|
||||
"append_content_stream": "capability_not_supported",
|
||||
"private_working_copy": "capability_not_supported",
|
||||
"full_cmis_sql_joins": "query_not_supported",
|
||||
"apply_policy": "capability_not_supported",
|
||||
"remove_policy": "capability_not_supported",
|
||||
"retention_hold_mutation": "capability_not_supported",
|
||||
"bulk_update_properties": "capability_not_supported",
|
||||
"rendition_streams": "capability_not_supported"
|
||||
},
|
||||
"profile_expectations": {
|
||||
"readonly-browser": {
|
||||
"must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"],
|
||||
"must_not_expose_objects": ["doc-confidential-risk"],
|
||||
"must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"]
|
||||
},
|
||||
"governed-authoring": {
|
||||
"must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"],
|
||||
"must_not_expose_objects": ["doc-confidential-risk"],
|
||||
"must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"]
|
||||
},
|
||||
"admin-export": {
|
||||
"must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log", "retention-renditions-bulk"],
|
||||
"must_not_expose_objects": [],
|
||||
"must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"]
|
||||
},
|
||||
"compat-tck": {
|
||||
"must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"],
|
||||
"must_not_expose_objects": ["doc-confidential-risk"],
|
||||
"must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,5 @@ markers = [
|
||||
"integration: tests that exercise optional external package contracts",
|
||||
"markitect_tool: tests for the optional markitect-tool adapter boundary",
|
||||
"capacity: opt-in capacity sentinel tests for bottleneck and scaling risks",
|
||||
"cmis: tests for CMIS compatibility fixtures and adapter contracts",
|
||||
]
|
||||
|
||||
@@ -23,6 +23,11 @@ from .core import (
|
||||
AuditEvent,
|
||||
AuditOutcome,
|
||||
Classification,
|
||||
CMISAccessPoint,
|
||||
CMISAccessProfile,
|
||||
CMISAction,
|
||||
CMISBinding,
|
||||
CMISCapability,
|
||||
ConnectorCapability,
|
||||
ContextEntity,
|
||||
ContextEntityType,
|
||||
@@ -167,6 +172,11 @@ __all__ = [
|
||||
"BatchItemResult",
|
||||
"BatchOperationResult",
|
||||
"Classification",
|
||||
"CMISAccessPoint",
|
||||
"CMISAccessProfile",
|
||||
"CMISAction",
|
||||
"CMISBinding",
|
||||
"CMISCapability",
|
||||
"ConnectorCapability",
|
||||
"Collection",
|
||||
"ContextAssembler",
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
from .actors import Actor, ActorType, OperationContext
|
||||
from .assets import AssetRepresentation, KnowledgeAsset, RepresentationKind
|
||||
from .audit import AuditEvent, AuditOutcome
|
||||
from .cmis import (
|
||||
CMISAccessPoint,
|
||||
CMISAccessProfile,
|
||||
CMISAction,
|
||||
CMISBinding,
|
||||
CMISCapability,
|
||||
)
|
||||
from .idempotency import IdempotencyRecord, IdempotencyStatus
|
||||
from .ingestion import (
|
||||
ConnectorCapability,
|
||||
@@ -67,6 +74,11 @@ __all__ = [
|
||||
"AuditEvent",
|
||||
"AuditOutcome",
|
||||
"Classification",
|
||||
"CMISAccessPoint",
|
||||
"CMISAccessProfile",
|
||||
"CMISAction",
|
||||
"CMISBinding",
|
||||
"CMISCapability",
|
||||
"ConnectorCapability",
|
||||
"ContextEntity",
|
||||
"ContextEntityType",
|
||||
|
||||
427
src/kontextual_engine/core/cmis.py
Normal file
427
src/kontextual_engine/core/cmis.py
Normal file
@@ -0,0 +1,427 @@
|
||||
"""CMIS access-point profile primitives.
|
||||
|
||||
These classes model how a CMIS adapter may expose engine capabilities without
|
||||
turning CMIS into a second domain model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from .actors import ActorType, OperationContext
|
||||
from .assets import KnowledgeAsset
|
||||
from .metadata import Sensitivity
|
||||
from .policy import PolicyDecision
|
||||
from .primitives import compact_dict
|
||||
|
||||
|
||||
class CMISBinding(str, Enum):
|
||||
BROWSER = "browser"
|
||||
ATOMPUB = "atompub"
|
||||
WEB_SERVICES = "web_services"
|
||||
|
||||
|
||||
class CMISCapability(str, Enum):
|
||||
REPOSITORY = "repository"
|
||||
TYPE_DEFINITIONS = "type_definitions"
|
||||
NAVIGATION = "navigation"
|
||||
OBJECT_READ = "object_read"
|
||||
OBJECT_WRITE = "object_write"
|
||||
CONTENT_STREAM_READ = "content_stream_read"
|
||||
CONTENT_STREAM_WRITE = "content_stream_write"
|
||||
VERSIONING = "versioning"
|
||||
DISCOVERY_QUERY = "discovery_query"
|
||||
RELATIONSHIPS = "relationships"
|
||||
ACL = "acl"
|
||||
POLICY = "policy"
|
||||
CHANGE_LOG = "change_log"
|
||||
RENDITIONS = "renditions"
|
||||
RETENTION_HOLD = "retention_hold"
|
||||
BULK_UPDATE = "bulk_update"
|
||||
|
||||
|
||||
class CMISAction(str, Enum):
|
||||
GET_REPOSITORY_INFO = "get_repository_info"
|
||||
GET_TYPE_DEFINITION = "get_type_definition"
|
||||
GET_CHILDREN = "get_children"
|
||||
GET_OBJECT = "get_object"
|
||||
GET_CONTENT_STREAM = "get_content_stream"
|
||||
QUERY = "query"
|
||||
GET_RELATIONSHIPS = "get_relationships"
|
||||
GET_CHANGE_LOG = "get_change_log"
|
||||
CREATE_DOCUMENT = "create_document"
|
||||
UPDATE_PROPERTIES = "update_properties"
|
||||
DELETE_OBJECT = "delete_object"
|
||||
SET_CONTENT_STREAM = "set_content_stream"
|
||||
APPLY_ACL = "apply_acl"
|
||||
APPLY_POLICY = "apply_policy"
|
||||
BULK_UPDATE_PROPERTIES = "bulk_update_properties"
|
||||
|
||||
|
||||
ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
||||
CMISAction.GET_REPOSITORY_INFO: CMISCapability.REPOSITORY,
|
||||
CMISAction.GET_TYPE_DEFINITION: CMISCapability.TYPE_DEFINITIONS,
|
||||
CMISAction.GET_CHILDREN: CMISCapability.NAVIGATION,
|
||||
CMISAction.GET_OBJECT: CMISCapability.OBJECT_READ,
|
||||
CMISAction.GET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_READ,
|
||||
CMISAction.QUERY: CMISCapability.DISCOVERY_QUERY,
|
||||
CMISAction.GET_RELATIONSHIPS: CMISCapability.RELATIONSHIPS,
|
||||
CMISAction.GET_CHANGE_LOG: CMISCapability.CHANGE_LOG,
|
||||
CMISAction.CREATE_DOCUMENT: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.UPDATE_PROPERTIES: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.DELETE_OBJECT: CMISCapability.OBJECT_WRITE,
|
||||
CMISAction.SET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_WRITE,
|
||||
CMISAction.APPLY_ACL: CMISCapability.ACL,
|
||||
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
|
||||
CMISAction.BULK_UPDATE_PROPERTIES: CMISCapability.BULK_UPDATE,
|
||||
}
|
||||
MUTATION_ACTIONS = {
|
||||
CMISAction.CREATE_DOCUMENT,
|
||||
CMISAction.UPDATE_PROPERTIES,
|
||||
CMISAction.DELETE_OBJECT,
|
||||
CMISAction.SET_CONTENT_STREAM,
|
||||
CMISAction.APPLY_ACL,
|
||||
CMISAction.APPLY_POLICY,
|
||||
CMISAction.BULK_UPDATE_PROPERTIES,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CMISAccessProfile:
|
||||
name: str
|
||||
binding: CMISBinding | str = CMISBinding.BROWSER
|
||||
capabilities: tuple[CMISCapability | str, ...] = ()
|
||||
allow_mutations: bool = False
|
||||
visible_sensitivities: tuple[Sensitivity | str, ...] = (Sensitivity.PUBLIC, Sensitivity.INTERNAL)
|
||||
denied_sensitivities: tuple[Sensitivity | str, ...] = (Sensitivity.CONFIDENTIAL, Sensitivity.RESTRICTED)
|
||||
visible_asset_types: tuple[str, ...] = ()
|
||||
visible_topics: tuple[str, ...] = ()
|
||||
visible_source_systems: tuple[str, ...] = ()
|
||||
denied_metadata: dict[str, tuple[Any, ...]] = field(default_factory=dict)
|
||||
required_actor_types: tuple[ActorType | str, ...] = ()
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "binding", CMISBinding(self.binding))
|
||||
object.__setattr__(
|
||||
self,
|
||||
"capabilities",
|
||||
tuple(CMISCapability(capability) for capability in self.capabilities),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"visible_sensitivities",
|
||||
tuple(Sensitivity(value) for value in self.visible_sensitivities),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"denied_sensitivities",
|
||||
tuple(Sensitivity(value) for value in self.denied_sensitivities),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"required_actor_types",
|
||||
tuple(ActorType(value) for value in self.required_actor_types),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"denied_metadata",
|
||||
{key: tuple(values) for key, values in self.denied_metadata.items()},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def readonly_browser(cls) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name="readonly-browser",
|
||||
capabilities=_read_capabilities(),
|
||||
allow_mutations=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def governed_authoring(cls) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name="governed-authoring",
|
||||
capabilities=_read_capabilities()
|
||||
+ (
|
||||
CMISCapability.OBJECT_WRITE,
|
||||
CMISCapability.CONTENT_STREAM_WRITE,
|
||||
),
|
||||
allow_mutations=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def admin_export(cls) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name="admin-export",
|
||||
capabilities=_read_capabilities()
|
||||
+ (
|
||||
CMISCapability.RENDITIONS,
|
||||
CMISCapability.RETENTION_HOLD,
|
||||
),
|
||||
allow_mutations=False,
|
||||
visible_sensitivities=(
|
||||
Sensitivity.PUBLIC,
|
||||
Sensitivity.INTERNAL,
|
||||
Sensitivity.CONFIDENTIAL,
|
||||
Sensitivity.RESTRICTED,
|
||||
),
|
||||
denied_sensitivities=(),
|
||||
required_actor_types=(ActorType.SERVICE_ACCOUNT,),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def compat_tck(cls) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name="compat-tck",
|
||||
capabilities=_read_capabilities()
|
||||
+ (
|
||||
CMISCapability.OBJECT_WRITE,
|
||||
CMISCapability.CONTENT_STREAM_WRITE,
|
||||
),
|
||||
allow_mutations=True,
|
||||
metadata={"compatibility": "selected-opencmis-tck-browser-subset"},
|
||||
)
|
||||
|
||||
def has_capability(self, capability: CMISCapability | str) -> bool:
|
||||
return CMISCapability(capability) in self.capabilities
|
||||
|
||||
def allows_action(self, action: CMISAction | str) -> bool:
|
||||
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
|
||||
|
||||
def decide_action(
|
||||
self,
|
||||
action: CMISAction | str,
|
||||
context: OperationContext,
|
||||
*,
|
||||
resource: str = "cmis:repository",
|
||||
) -> PolicyDecision:
|
||||
cmis_action = CMISAction(action)
|
||||
if not self.allows_actor(context):
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
cmis_action.value,
|
||||
resource,
|
||||
reason="actor_type_not_allowed_for_cmis_profile",
|
||||
context={"profile": self.name},
|
||||
)
|
||||
if self.allows_action(cmis_action):
|
||||
return PolicyDecision.allow(
|
||||
context.actor.id,
|
||||
cmis_action.value,
|
||||
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"
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
cmis_action.value,
|
||||
resource,
|
||||
reason=reason,
|
||||
context={"profile": self.name, "capability": capability.value},
|
||||
)
|
||||
|
||||
def allows_actor(self, context: OperationContext) -> bool:
|
||||
return not self.required_actor_types or context.actor.actor_type in self.required_actor_types
|
||||
|
||||
def exposes_asset(self, asset: KnowledgeAsset, context: OperationContext) -> bool:
|
||||
return self.decide_asset_visibility(asset, context).allowed
|
||||
|
||||
def decide_asset_visibility(
|
||||
self,
|
||||
asset: KnowledgeAsset,
|
||||
context: OperationContext,
|
||||
) -> PolicyDecision:
|
||||
resource = f"asset:{asset.id}"
|
||||
if not self.allows_actor(context):
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="actor_type_not_allowed_for_cmis_profile",
|
||||
context={"profile": self.name},
|
||||
)
|
||||
classification = asset.classification
|
||||
if classification.sensitivity in self.denied_sensitivities:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_sensitivity_denied",
|
||||
context={"profile": self.name, "sensitivity": _enum_value(classification.sensitivity)},
|
||||
)
|
||||
if classification.sensitivity not in self.visible_sensitivities:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_sensitivity_not_visible",
|
||||
context={"profile": self.name, "sensitivity": _enum_value(classification.sensitivity)},
|
||||
)
|
||||
if self.visible_asset_types and classification.asset_type not in self.visible_asset_types:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_asset_type_not_visible",
|
||||
context={"profile": self.name, "asset_type": classification.asset_type},
|
||||
)
|
||||
if self.visible_topics and not set(classification.topics).intersection(self.visible_topics):
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_topic_not_visible",
|
||||
context={"profile": self.name, "topics": list(classification.topics)},
|
||||
)
|
||||
source_system = asset.metadata.get("source_system") or classification.metadata.get("source_system")
|
||||
if self.visible_source_systems and source_system not in self.visible_source_systems:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_source_system_not_visible",
|
||||
context={"profile": self.name, "source_system": source_system},
|
||||
)
|
||||
for key, denied_values in self.denied_metadata.items():
|
||||
value = asset.metadata.get(key, classification.metadata.get(key))
|
||||
if value in denied_values:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
reason="cmis_metadata_denied",
|
||||
context={"profile": self.name, "metadata_key": key},
|
||||
)
|
||||
return PolicyDecision.allow(
|
||||
context.actor.id,
|
||||
"cmis.expose_asset",
|
||||
resource,
|
||||
context={"profile": self.name},
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return compact_dict(
|
||||
{
|
||||
"name": self.name,
|
||||
"binding": self.binding.value,
|
||||
"capabilities": [capability.value for capability in self.capabilities],
|
||||
"allow_mutations": self.allow_mutations,
|
||||
"visible_sensitivities": [item.value for item in self.visible_sensitivities],
|
||||
"denied_sensitivities": [item.value for item in self.denied_sensitivities],
|
||||
"visible_asset_types": list(self.visible_asset_types),
|
||||
"visible_topics": list(self.visible_topics),
|
||||
"visible_source_systems": list(self.visible_source_systems),
|
||||
"denied_metadata": {key: list(values) for key, values in self.denied_metadata.items()},
|
||||
"required_actor_types": [item.value for item in self.required_actor_types],
|
||||
"metadata": dict(self.metadata),
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "CMISAccessProfile":
|
||||
return cls(
|
||||
name=data["name"],
|
||||
binding=data.get("binding", CMISBinding.BROWSER.value),
|
||||
capabilities=tuple(data.get("capabilities", [])),
|
||||
allow_mutations=bool(data.get("allow_mutations", False)),
|
||||
visible_sensitivities=tuple(data.get("visible_sensitivities", [])),
|
||||
denied_sensitivities=tuple(data.get("denied_sensitivities", [])),
|
||||
visible_asset_types=tuple(data.get("visible_asset_types", [])),
|
||||
visible_topics=tuple(data.get("visible_topics", [])),
|
||||
visible_source_systems=tuple(data.get("visible_source_systems", [])),
|
||||
denied_metadata=dict(data.get("denied_metadata", {})),
|
||||
required_actor_types=tuple(data.get("required_actor_types", [])),
|
||||
metadata=dict(data.get("metadata", {})),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CMISAccessPoint:
|
||||
access_point_id: str
|
||||
repository_id: str
|
||||
profile: CMISAccessProfile
|
||||
base_path: str
|
||||
root_folder_id: str = "cmis-root"
|
||||
enabled: bool = True
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
normalized = "/" + self.base_path.strip("/")
|
||||
object.__setattr__(self, "base_path", normalized)
|
||||
|
||||
def decide_action(
|
||||
self,
|
||||
action: CMISAction | str,
|
||||
context: OperationContext,
|
||||
*,
|
||||
resource: str | None = None,
|
||||
) -> PolicyDecision:
|
||||
if not self.enabled:
|
||||
return PolicyDecision.deny(
|
||||
context.actor.id,
|
||||
CMISAction(action).value,
|
||||
resource or f"cmis:{self.repository_id}",
|
||||
reason="cmis_access_point_disabled",
|
||||
context={"access_point_id": self.access_point_id, "profile": self.profile.name},
|
||||
)
|
||||
return self.profile.decide_action(
|
||||
action,
|
||||
context,
|
||||
resource=resource or f"cmis:{self.repository_id}",
|
||||
)
|
||||
|
||||
def exposes_asset(self, asset: KnowledgeAsset, context: OperationContext) -> bool:
|
||||
return self.enabled and self.profile.exposes_asset(asset, context)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return compact_dict(
|
||||
{
|
||||
"access_point_id": self.access_point_id,
|
||||
"repository_id": self.repository_id,
|
||||
"profile": self.profile.to_dict(),
|
||||
"base_path": self.base_path,
|
||||
"root_folder_id": self.root_folder_id,
|
||||
"enabled": self.enabled,
|
||||
"metadata": dict(self.metadata),
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "CMISAccessPoint":
|
||||
return cls(
|
||||
access_point_id=data["access_point_id"],
|
||||
repository_id=data["repository_id"],
|
||||
profile=CMISAccessProfile.from_dict(data["profile"]),
|
||||
base_path=data["base_path"],
|
||||
root_folder_id=data.get("root_folder_id", "cmis-root"),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
metadata=dict(data.get("metadata", {})),
|
||||
)
|
||||
|
||||
|
||||
def _read_capabilities() -> tuple[CMISCapability, ...]:
|
||||
return (
|
||||
CMISCapability.REPOSITORY,
|
||||
CMISCapability.TYPE_DEFINITIONS,
|
||||
CMISCapability.NAVIGATION,
|
||||
CMISCapability.OBJECT_READ,
|
||||
CMISCapability.CONTENT_STREAM_READ,
|
||||
CMISCapability.VERSIONING,
|
||||
CMISCapability.DISCOVERY_QUERY,
|
||||
CMISCapability.RELATIONSHIPS,
|
||||
CMISCapability.ACL,
|
||||
CMISCapability.CHANGE_LOG,
|
||||
)
|
||||
|
||||
|
||||
def _enum_value(value: Any) -> Any:
|
||||
return getattr(value, "value", value)
|
||||
28
tests/cmis/opencmis-tck/README.md
Normal file
28
tests/cmis/opencmis-tck/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Optional OpenCMIS TCK Harness
|
||||
|
||||
This directory reserves the integration-test boundary for Apache Chemistry
|
||||
OpenCMIS TCK 1.1.0. The harness is intentionally optional because Java/Maven
|
||||
tooling is not part of the default Python test suite.
|
||||
|
||||
Planned invocation shape:
|
||||
|
||||
```sh
|
||||
mvn -Dcmis.browser.url=http://127.0.0.1:8000/cmis/compat-tck/browser \
|
||||
-Dcmis.repository.id=kontextual-compat-tck \
|
||||
test
|
||||
```
|
||||
|
||||
The first target is a selected Browser Binding subset:
|
||||
|
||||
- repository information,
|
||||
- type definitions,
|
||||
- navigation,
|
||||
- object reads,
|
||||
- content stream reads,
|
||||
- query subset,
|
||||
- relationships,
|
||||
- change log where supported.
|
||||
|
||||
Mutation groups should only be enabled for `governed-authoring` or
|
||||
`compat-tck` profiles after the policy and audit gates are implemented.
|
||||
|
||||
151
tests/cmis/test_cmis_access_profiles.py
Normal file
151
tests/cmis/test_cmis_access_profiles.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from kontextual_engine import (
|
||||
Actor,
|
||||
ActorType,
|
||||
CMISAccessPoint,
|
||||
CMISAccessProfile,
|
||||
CMISAction,
|
||||
CMISCapability,
|
||||
Classification,
|
||||
KnowledgeAsset,
|
||||
OperationContext,
|
||||
)
|
||||
|
||||
|
||||
def _context(actor_type: ActorType = ActorType.HUMAN) -> OperationContext:
|
||||
return OperationContext.create(
|
||||
Actor.create(actor_type, actor_id=f"actor-{actor_type.value}"),
|
||||
correlation_id="corr-cmis-profile",
|
||||
)
|
||||
|
||||
|
||||
def _asset(
|
||||
asset_id: str,
|
||||
sensitivity: str,
|
||||
*,
|
||||
asset_type: str = "document",
|
||||
topics: tuple[str, ...] = ("cmis",),
|
||||
metadata: dict | None = None,
|
||||
) -> KnowledgeAsset:
|
||||
return KnowledgeAsset.create(
|
||||
f"Asset {asset_id}",
|
||||
Classification(asset_type=asset_type, sensitivity=sensitivity, topics=topics),
|
||||
asset_id=asset_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
def test_readonly_profile_exposes_read_capabilities_and_rejects_mutations() -> None:
|
||||
profile = CMISAccessProfile.readonly_browser()
|
||||
context = _context()
|
||||
|
||||
assert profile.has_capability(CMISCapability.REPOSITORY)
|
||||
assert profile.has_capability(CMISCapability.CONTENT_STREAM_READ)
|
||||
assert profile.decide_action(CMISAction.GET_OBJECT, context).allowed is True
|
||||
|
||||
mutation = profile.decide_action(CMISAction.CREATE_DOCUMENT, context)
|
||||
assert mutation.allowed is False
|
||||
assert mutation.reason == "cmis_mutation_not_allowed"
|
||||
|
||||
|
||||
def test_governed_authoring_profile_allows_selected_write_actions() -> None:
|
||||
profile = CMISAccessProfile.governed_authoring()
|
||||
context = _context()
|
||||
|
||||
assert profile.decide_action(CMISAction.CREATE_DOCUMENT, context).allowed is True
|
||||
assert profile.decide_action(CMISAction.SET_CONTENT_STREAM, context).allowed is True
|
||||
assert profile.decide_action(CMISAction.BULK_UPDATE_PROPERTIES, context).allowed is False
|
||||
|
||||
|
||||
def test_profiles_hide_denied_sensitivities_without_partial_exposure() -> None:
|
||||
profile = CMISAccessProfile.readonly_browser()
|
||||
context = _context()
|
||||
public = _asset("asset-public", "public")
|
||||
confidential = _asset("asset-confidential", "confidential")
|
||||
|
||||
assert profile.exposes_asset(public, context) is True
|
||||
|
||||
denied = profile.decide_asset_visibility(confidential, context)
|
||||
assert denied.allowed is False
|
||||
assert denied.reason == "cmis_sensitivity_denied"
|
||||
assert denied.context["sensitivity"] == "confidential"
|
||||
|
||||
|
||||
def test_profile_visibility_can_be_scoped_by_type_topic_source_and_metadata() -> None:
|
||||
profile = CMISAccessProfile(
|
||||
name="topic-source-scope",
|
||||
capabilities=(CMISCapability.OBJECT_READ,),
|
||||
visible_asset_types=("document",),
|
||||
visible_topics=("approved",),
|
||||
visible_source_systems=("sharepoint",),
|
||||
denied_metadata={"export_blocked": (True,)},
|
||||
)
|
||||
context = _context()
|
||||
|
||||
allowed = _asset(
|
||||
"asset-allowed",
|
||||
"internal",
|
||||
topics=("approved",),
|
||||
metadata={"source_system": "sharepoint"},
|
||||
)
|
||||
wrong_topic = _asset(
|
||||
"asset-wrong-topic",
|
||||
"internal",
|
||||
topics=("draft",),
|
||||
metadata={"source_system": "sharepoint"},
|
||||
)
|
||||
blocked = _asset(
|
||||
"asset-blocked",
|
||||
"internal",
|
||||
topics=("approved",),
|
||||
metadata={"source_system": "sharepoint", "export_blocked": True},
|
||||
)
|
||||
|
||||
assert profile.exposes_asset(allowed, context) is True
|
||||
assert profile.decide_asset_visibility(wrong_topic, context).reason == "cmis_topic_not_visible"
|
||||
assert profile.decide_asset_visibility(blocked, context).reason == "cmis_metadata_denied"
|
||||
|
||||
|
||||
def test_admin_export_requires_service_account_actor() -> None:
|
||||
profile = CMISAccessProfile.admin_export()
|
||||
human_context = _context(ActorType.HUMAN)
|
||||
service_context = _context(ActorType.SERVICE_ACCOUNT)
|
||||
confidential = _asset("asset-confidential", "confidential")
|
||||
|
||||
assert profile.decide_action(CMISAction.GET_OBJECT, human_context).allowed is False
|
||||
assert profile.exposes_asset(confidential, human_context) is False
|
||||
assert profile.decide_action(CMISAction.GET_OBJECT, service_context).allowed is True
|
||||
assert profile.exposes_asset(confidential, service_context) is True
|
||||
|
||||
|
||||
def test_access_point_normalizes_base_path_and_round_trips() -> None:
|
||||
access_point = CMISAccessPoint(
|
||||
access_point_id="cmis-readonly",
|
||||
repository_id="kontextual-readonly",
|
||||
profile=CMISAccessProfile.readonly_browser(),
|
||||
base_path="cmis/readonly/browser",
|
||||
metadata={"owner": "codex"},
|
||||
)
|
||||
|
||||
serialized = access_point.to_dict()
|
||||
restored = CMISAccessPoint.from_dict(serialized)
|
||||
|
||||
assert access_point.base_path == "/cmis/readonly/browser"
|
||||
assert restored == access_point
|
||||
assert restored.decide_action(CMISAction.GET_REPOSITORY_INFO, _context()).allowed is True
|
||||
|
||||
|
||||
def test_disabled_access_point_denies_all_actions_and_visibility() -> None:
|
||||
access_point = CMISAccessPoint(
|
||||
access_point_id="cmis-disabled",
|
||||
repository_id="kontextual-disabled",
|
||||
profile=CMISAccessProfile.governed_authoring(),
|
||||
base_path="/cmis/disabled/browser",
|
||||
enabled=False,
|
||||
)
|
||||
context = _context()
|
||||
|
||||
assert access_point.decide_action(CMISAction.GET_OBJECT, context).reason == "cmis_access_point_disabled"
|
||||
assert access_point.exposes_asset(_asset("asset-public", "public"), context) is False
|
||||
|
||||
120
tests/cmis/test_cmis_contract_examples.py
Normal file
120
tests/cmis/test_cmis_contract_examples.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FIXTURE_PATH = (
|
||||
Path(__file__).resolve().parents[2] / "examples" / "cmis" / "capability-fixtures.json"
|
||||
)
|
||||
|
||||
REQUIRED_GROUPS = {
|
||||
"repository-type",
|
||||
"navigation",
|
||||
"object-content",
|
||||
"versioning",
|
||||
"discovery-query",
|
||||
"relationships",
|
||||
"acl-policy",
|
||||
"change-log",
|
||||
"retention-renditions-bulk",
|
||||
}
|
||||
REQUIRED_PROFILES = {"readonly-browser", "governed-authoring", "admin-export", "compat-tck"}
|
||||
|
||||
|
||||
def _catalog() -> dict:
|
||||
return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_cmis_fixture_catalog_declares_target_standard_and_profiles() -> None:
|
||||
catalog = _catalog()
|
||||
|
||||
assert catalog["standard"] == {
|
||||
"name": "CMIS",
|
||||
"version": "1.1",
|
||||
"primary_binding": "browser",
|
||||
"deferred_bindings": ["atompub", "web_services"],
|
||||
}
|
||||
assert set(catalog["profiles"]) == REQUIRED_PROFILES
|
||||
assert all(profile["binding"] == "browser" for profile in catalog["profiles"].values())
|
||||
|
||||
|
||||
def test_cmis_fixture_groups_cover_required_capabilities() -> None:
|
||||
catalog = _catalog()
|
||||
groups = {group["id"]: group for group in catalog["capability_groups"]}
|
||||
|
||||
assert set(groups) == REQUIRED_GROUPS
|
||||
for group in groups.values():
|
||||
assert group["examples"], group["id"]
|
||||
assert group["must_validate"], group["id"]
|
||||
assert set(group["supported_profiles"]).issubset(REQUIRED_PROFILES)
|
||||
|
||||
|
||||
def test_unsupported_cmis_features_have_structured_diagnostics() -> None:
|
||||
catalog = _catalog()
|
||||
diagnostics = catalog["unsupported_diagnostics"]
|
||||
|
||||
unsupported = {
|
||||
feature
|
||||
for group in catalog["capability_groups"]
|
||||
for feature in group.get("unsupported", [])
|
||||
}
|
||||
unsupported.update(catalog["standard"]["deferred_bindings"])
|
||||
|
||||
assert unsupported <= set(diagnostics)
|
||||
assert all(diagnostics[feature] for feature in unsupported)
|
||||
|
||||
|
||||
def test_profile_visibility_and_mutation_expectations_are_explicit() -> None:
|
||||
catalog = _catalog()
|
||||
groups = {group["id"] for group in catalog["capability_groups"]}
|
||||
|
||||
for profile_name, profile in catalog["profiles"].items():
|
||||
expectations = catalog["profile_expectations"][profile_name]
|
||||
|
||||
assert set(expectations["must_expose"]).issubset(groups)
|
||||
assert "must_not_expose_objects" in expectations
|
||||
if profile["mutations"]:
|
||||
assert expectations["must_authorize_actions"] == [
|
||||
"create_document",
|
||||
"update_properties",
|
||||
"delete_object",
|
||||
"set_content_stream",
|
||||
]
|
||||
else:
|
||||
assert expectations["must_reject_actions"] == [
|
||||
"create_document",
|
||||
"update_properties",
|
||||
"delete_object",
|
||||
"set_content_stream",
|
||||
]
|
||||
|
||||
|
||||
def test_readonly_and_governed_profiles_hide_confidential_fixture_objects() -> None:
|
||||
catalog = _catalog()
|
||||
objects = {item["id"]: item for item in catalog["objects"]}
|
||||
confidential = {
|
||||
object_id
|
||||
for object_id, item in objects.items()
|
||||
if item.get("sensitivity") == "confidential"
|
||||
}
|
||||
|
||||
assert confidential
|
||||
for profile_name in ["readonly-browser", "governed-authoring", "compat-tck"]:
|
||||
denied = set(catalog["profile_expectations"][profile_name]["must_not_expose_objects"])
|
||||
assert confidential <= denied
|
||||
|
||||
|
||||
def test_admin_export_is_the_only_profile_for_deferred_extension_group() -> None:
|
||||
catalog = _catalog()
|
||||
extension = next(
|
||||
group
|
||||
for group in catalog["capability_groups"]
|
||||
if group["id"] == "retention-renditions-bulk"
|
||||
)
|
||||
|
||||
assert extension["supported_profiles"] == ["admin-export"]
|
||||
for profile_name, expectations in catalog["profile_expectations"].items():
|
||||
exposes_extension = "retention-renditions-bulk" in expectations["must_expose"]
|
||||
assert exposes_extension is (profile_name == "admin-export")
|
||||
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "CMIS Compliance Assessment And Test Foundation"
|
||||
domain: markitect
|
||||
repo: kontextual-engine
|
||||
status: active
|
||||
status: completed
|
||||
owner: codex
|
||||
topic_slug: markitect
|
||||
planning_priority: high
|
||||
@@ -27,6 +27,9 @@ compliance tests.
|
||||
|
||||
- `docs/cmis-compliance-assessment.md`
|
||||
- `docs/cmis-compliance-test-foundation.md`
|
||||
- `docs/cmis-readiness-gate.md`
|
||||
- `examples/cmis/capability-fixtures.json`
|
||||
- `tests/cmis/test_cmis_contract_examples.py`
|
||||
- OASIS CMIS 1.1 standard and errata.
|
||||
- Apache Chemistry OpenCMIS TCK 1.1.0 and CMIS Workbench as optional external
|
||||
validation tools.
|
||||
@@ -41,7 +44,7 @@ fixtures, profile matrix, and test harness that will govern implementation in
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T001
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1f4ed133-74f7-46df-9266-277813b5399a"
|
||||
```
|
||||
@@ -56,7 +59,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T002
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "02651630-16e6-4c5f-879d-988a55fb7227"
|
||||
```
|
||||
@@ -71,7 +74,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T003
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a895218c-61b1-4944-8774-fd21c5416580"
|
||||
```
|
||||
@@ -88,7 +91,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T004
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "710da9b6-2034-4948-8bc7-16230cc839cf"
|
||||
```
|
||||
@@ -104,7 +107,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T005
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "85b321a1-b9b5-469e-bc45-b207dc39ad7a"
|
||||
```
|
||||
@@ -119,7 +122,7 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: KONT-WP-0011-T006
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "604e2f06-fde5-4d88-a13e-f3b725177696"
|
||||
```
|
||||
|
||||
@@ -38,6 +38,12 @@ Implementation must begin after the assessment, examples, and test foundation
|
||||
from `KONT-WP-0011` are sufficient to define the first profile and regression
|
||||
suite.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `docs/cmis-profiled-access-points-implementation.md`
|
||||
- `src/kontextual_engine/core/cmis.py`
|
||||
- `tests/cmis/test_cmis_access_profiles.py`
|
||||
|
||||
## Architecture Constraint
|
||||
|
||||
CMIS routes are adapters over engine services and policy gates. They must not
|
||||
@@ -49,7 +55,7 @@ emits audit events.
|
||||
|
||||
```task
|
||||
id: KONT-WP-0012-T001
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "031c3ce5-bb56-41fb-a014-6a496c280d20"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user