generated from coulomb/repo-seed
CMIS layer into an honest CMIS 1.1
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
# CMIS Compliance Assessment
|
# CMIS Compliance Assessment
|
||||||
|
|
||||||
Date: 2026-05-06
|
Date: 2026-05-07
|
||||||
|
|
||||||
Status: planning baseline for CMIS compliance and access-point implementation.
|
Status: Browser Binding subset implemented with conservative CMIS capability
|
||||||
|
flags and explicit unsupported diagnostics.
|
||||||
|
|
||||||
## Reference Standard
|
## Reference Standard
|
||||||
|
|
||||||
@@ -42,35 +43,36 @@ Practical strategy:
|
|||||||
|
|
||||||
| CMIS capability | Current engine availability | Gap | Demand |
|
| CMIS capability | Current engine availability | Gap | Demand |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Repository service | Service health/version, runtime repository state, capability catalogs. | Need CMIS repository info, repository IDs, root folder IDs, capability flags, type summaries. | Low |
|
| Repository service | Implemented. | Repository info includes CMIS 1.1 identity, complete conservative optional capability flags, repository features, and unsupported feature diagnostics. | Low |
|
||||||
| Type definitions | Asset classifications, metadata schemas, relationship target kinds. | Need CMIS base types, property definitions, type mutability flags, secondary type projection. | Medium |
|
| Type definitions | Implemented subset. | Base type projections exist; type mutability, CMIS versioning, folder ACL control, and non-document querying are explicitly not advertised. | Low |
|
||||||
| Navigation service | Relationships and context graph exist, but no folder tree model. | Need root folder, folder children, descendants/tree, parent relationships, path semantics. | High |
|
| Navigation service | Implemented subset. | `getChildren` and projected parents are supported. `getDescendants`, `getFolderTree`, mutating multifiling, and unfiling are explicitly flagged unsupported. | Low unless full folder tree is required |
|
||||||
| Object service read | Assets, metadata, representations, content refs, audit, versions exist. | Need CMIS object envelopes, allowable actions, path/object-id lookup, property filters, rendition/content stream response shape. | Medium |
|
| Object service read | Implemented subset. | Object envelopes, allowable actions, content stream descriptors, content stream properties, visibility redaction, and relationship IDs are covered. | Low |
|
||||||
| Object service write | Asset create, metadata add, lifecycle transition, relationship create, ingestion. | Need createDocument/createFolder/updateProperties/deleteObject/moveObject mapping and CMIS change tokens. | High |
|
| Object service write | Governed subset. | `createDocument`, custom metadata updates, `setContentStream`, and delete-request lifecycle transition are supported by authoring profiles. Unsupported standard property updates now fail with diagnostics. | Medium |
|
||||||
| Content streams | Source, normalized, derived representations store content hashes and storage refs. | Need getContentStream/setContentStream/deleteContentStream/appendContentStream semantics and streaming endpoints. | Medium-High |
|
| Content streams | Implemented subset. | Descriptor and byte-stream routes exist; `setContentStream` writes through deduplicating blob storage. Append/delete content stream are unsupported. | Low |
|
||||||
| Versioning | Asset versions and transformation/workflow lineage exist. | Need CMIS checkout, PWC, checkin, cancelCheckout, version series semantics, latest/major flags. | High |
|
| Versioning | Projection only. | Latest-version properties can be projected from engine versions, but CMIS checkout/PWC/all-versions services are not advertised. | Low if unsupported remains acceptable |
|
||||||
| Discovery/query | Governed retrieval, lexical search, filters, relationships. | Need CMIS SQL-like query grammar or supported subset, query result shape, joins/capability flags. | High |
|
| Discovery/query | Implemented narrow subset. | `SELECT * FROM cmis:document` and `SELECT * FROM kontextual:document` are supported. Joins, order-by, full CMIS SQL predicates, and full-text are flagged unsupported. | Medium |
|
||||||
| Relationships | Core relationships exist. | Need CMIS relationship object mapping and relationship type capability exposure. | Medium |
|
| Relationships | Implemented subset. | Relationship object projections and source filters are covered and profile-gated. | Low |
|
||||||
| ACL service | Policy gateway and authorization decisions exist. | Need CMIS ACL model, principals, direct/inherited ACEs, applyACL, exact capability flags. | High |
|
| ACL service | Discover only. | ACL projection is supported; `applyACL` is not authorized even for authoring profiles and returns an unimplemented diagnostic. | Low |
|
||||||
| Policy service | Policy decisions and governance reports exist. | Need CMIS policy objects/applyPolicy/removePolicy/getAppliedPolicies mapping or explicit unsupported profile. | Medium |
|
| Policy service | Unsupported. | `applyPolicy`/`removePolicy` are explicitly unsupported; engine policy remains native, not CMIS policy objects. | Low |
|
||||||
| Change log | Audit events and correlation IDs exist. | Need CMIS change events, change tokens, object change entries, paging. | Medium |
|
| Change log | Implemented subset. | Audit-backed object-id change entries and paging are supported; full property-level change details are not advertised. | Low |
|
||||||
| Multi-filing/unfiling | Not modeled directly. | Need folder membership model or profile-level unsupported flags. | High if full support, Low if unsupported |
|
| Multi-filing/unfiling | Projection only. | Multiple virtual parents are exposed as a Kontextual repository feature, while CMIS `capabilityMultifiling` and unfiling stay false. | Low |
|
||||||
| Renditions | Representations exist, no rendition taxonomy. | Need rendition metadata and stream mapping for thumbnails/previews. | Medium |
|
| Renditions | Unsupported. | Capability is `none`; derived representations are not exposed as CMIS rendition streams. | Low |
|
||||||
| Retention and hold | Metadata/governance hooks exist, no first-class legal hold model. | Need retention/hold capabilities, apply/remove hold, retention date semantics. | High for full support |
|
| Retention and hold | Unsupported. | Not advertised; left as native governance metadata until a real integration requires CMIS legal-hold semantics. | Low |
|
||||||
| Bulk update | Metadata update pathways exist. | Need bulkUpdateProperties semantics, partial failure reporting, change tokens. | Medium |
|
| Bulk update | Unsupported. | `bulkUpdateProperties` is explicitly unsupported. | Low |
|
||||||
| Browser JSON binding | FastAPI JSON service already exists. | Need CMIS Browser Binding routes, selectors/actions, multipart/content stream behavior. | High |
|
| Browser JSON binding | FastAPI JSON service already exists. | Need CMIS Browser Binding routes, selectors/actions, multipart/content stream behavior. | High |
|
||||||
| AtomPub binding | No AtomPub/XML binding. | Need XML/Atom feed generation and protocol semantics. | Very High |
|
| AtomPub binding | No AtomPub/XML binding. | Need XML/Atom feed generation and protocol semantics. | Very High |
|
||||||
| Web Services binding | No SOAP stack. | Need WSDL/SOAP implementation. | Very High |
|
| Web Services binding | No SOAP stack. | Need WSDL/SOAP implementation. | Very High |
|
||||||
|
|
||||||
## Recommended Compliance Profile Strategy
|
## Recommended Compliance Profile Strategy
|
||||||
|
|
||||||
Start with a constrained CMIS 1.1 Browser Binding profile:
|
Maintain a constrained CMIS 1.1 Browser Binding profile:
|
||||||
|
|
||||||
- Repository, type, object read, content stream read, query subset,
|
- Repository, type, object read, content stream read, query subset,
|
||||||
relationships, change log, and navigation over a synthetic root/folder
|
relationships, change log, and navigation over a synthetic root/folder
|
||||||
projection.
|
projection.
|
||||||
- Explicitly unsupported or read-only: AtomPub, Web Services, full ACL mutation,
|
- Explicitly unsupported or read-only: AtomPub, Web Services, descendants/tree,
|
||||||
retention/hold, multifiling/unfiling, and full CMIS SQL joins.
|
full ACL mutation, retention/hold, mutating multifiling/unfiling, PWC/versioning
|
||||||
|
services, renditions, bulk updates, order-by, and full CMIS SQL joins.
|
||||||
|
|
||||||
Then expand by profile:
|
Then expand by profile:
|
||||||
|
|
||||||
@@ -83,11 +85,12 @@ Then expand by profile:
|
|||||||
|
|
||||||
## Risk Summary
|
## Risk Summary
|
||||||
|
|
||||||
The engine already has strong foundations for asset identity, metadata,
|
The engine has a sound Browser Binding subset so long as clients trust the
|
||||||
representations, relationships, versions, audit, policy, retrieval, and
|
advertised capabilities instead of assuming broad ECM behavior. The remaining
|
||||||
service APIs. The hard parts are not storage; they are CMIS protocol semantics:
|
hard parts are optional CMIS semantics that we intentionally do not advertise:
|
||||||
folder/path behavior, versioning/PWC semantics, CMIS query grammar, ACL shape,
|
folder tree/descendant services, mutating filing services, PWC/versioning
|
||||||
content stream actions, and binding-specific compatibility.
|
services, broad query grammar, ACL mutation, renditions, retention/hold, and
|
||||||
|
legacy bindings.
|
||||||
|
|
||||||
Best estimate:
|
Best estimate:
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Date: 2026-05-06
|
Date: 2026-05-06
|
||||||
|
|
||||||
Status: initial test foundation established for CMIS access-point work.
|
Status: active fixture foundation with conservative capability-flag tests.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@@ -57,7 +57,9 @@ Validates:
|
|||||||
|
|
||||||
- root folder children,
|
- root folder children,
|
||||||
- folder path lookup,
|
- folder path lookup,
|
||||||
- getChildren/getDescendants/getFolderTree,
|
- getChildren,
|
||||||
|
- projection-only parent folders,
|
||||||
|
- explicit unsupported flags for getDescendants and getFolderTree,
|
||||||
- profile restrictions on folder visibility.
|
- profile restrictions on folder visibility.
|
||||||
|
|
||||||
### Object And Content Stream Service
|
### Object And Content Stream Service
|
||||||
@@ -91,8 +93,8 @@ Validates:
|
|||||||
|
|
||||||
- version series projection,
|
- version series projection,
|
||||||
- latest version flags,
|
- latest version flags,
|
||||||
- checkout/checkin unsupported or supported per profile,
|
- checkout/checkin/PWC unsupported diagnostics,
|
||||||
- version history listing.
|
- all-versions search unsupported flags.
|
||||||
|
|
||||||
### Discovery Query Service
|
### Discovery Query Service
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ Validates:
|
|||||||
- query capability flags,
|
- query capability flags,
|
||||||
- supported subset behavior,
|
- supported subset behavior,
|
||||||
- paging,
|
- paging,
|
||||||
- error diagnostics for unsupported grammar.
|
- error diagnostics for unsupported grammar, joins, and ordering.
|
||||||
|
|
||||||
### Relationship Service
|
### Relationship Service
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ Validates:
|
|||||||
|
|
||||||
- allowable actions,
|
- allowable actions,
|
||||||
- ACL projection,
|
- ACL projection,
|
||||||
- applyACL/applyPolicy supported or unsupported by profile,
|
- applyACL/applyPolicy unsupported diagnostics,
|
||||||
- no protected metadata leakage on denial.
|
- no protected metadata leakage on denial.
|
||||||
|
|
||||||
### Change Log Events
|
### Change Log Events
|
||||||
@@ -171,7 +173,7 @@ Fixtures:
|
|||||||
Validates:
|
Validates:
|
||||||
|
|
||||||
- explicit capability flags,
|
- explicit capability flags,
|
||||||
- supported subset behavior,
|
- unsupported capability diagnostics,
|
||||||
- structured unsupported-operation diagnostics.
|
- structured unsupported-operation diagnostics.
|
||||||
|
|
||||||
## Capability Profile Test Matrix
|
## Capability Profile Test Matrix
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Date: 2026-05-07
|
Date: 2026-05-07
|
||||||
|
|
||||||
Status: Browser Binding MVP implemented with profiled access points.
|
Status: Browser Binding subset implemented with profiled access points,
|
||||||
|
conservative capability flags, and explicit unsupported diagnostics.
|
||||||
|
|
||||||
## Endpoint Setup
|
## Endpoint Setup
|
||||||
|
|
||||||
@@ -62,7 +63,9 @@ Actor context is passed through the existing service headers, especially:
|
|||||||
| Browser Binding repository info | yes | yes | yes | yes |
|
| Browser Binding repository info | yes | yes | yes | yes |
|
||||||
| Type definitions | yes | yes | yes | yes |
|
| Type definitions | yes | yes | yes | yes |
|
||||||
| Synthetic navigation | yes | yes | yes | yes |
|
| Synthetic navigation | yes | yes | yes | yes |
|
||||||
| Projection-only multifiling | yes | yes | yes | yes |
|
| Projection-only parent maps | yes | yes | yes | yes |
|
||||||
|
| CMIS `capabilityMultifiling` | no | no | no | no |
|
||||||
|
| Descendants/folder tree services | no | no | no | no |
|
||||||
| Object reads | yes | yes | yes | yes |
|
| Object reads | yes | yes | yes | yes |
|
||||||
| Content stream descriptors | yes | yes | yes | yes |
|
| Content stream descriptors | yes | yes | yes | yes |
|
||||||
| ACL projection | discover | discover | discover | discover |
|
| ACL projection | discover | discover | discover | discover |
|
||||||
@@ -70,7 +73,7 @@ Actor context is passed through the existing service headers, especially:
|
|||||||
| Change log projection | yes | yes | yes | yes |
|
| Change log projection | yes | yes | yes | yes |
|
||||||
| Query subset | document select only | document select only | document select only | document select only |
|
| Query subset | document select only | document select only | document select only | document select only |
|
||||||
| Create document | no | yes | no | yes |
|
| Create document | no | yes | no | yes |
|
||||||
| Update properties | no | yes | no | yes |
|
| Update properties | no | custom metadata only | no | custom metadata only |
|
||||||
| Set content stream | no | yes | no | yes |
|
| Set content stream | no | yes | no | yes |
|
||||||
| Delete object | no | delete-request lifecycle transition | no | delete-request lifecycle transition |
|
| Delete object | no | delete-request lifecycle transition | no | delete-request lifecycle transition |
|
||||||
| Confidential/restricted visibility | hidden | hidden | service-account visible | hidden |
|
| Confidential/restricted visibility | hidden | hidden | service-account visible | hidden |
|
||||||
@@ -104,6 +107,7 @@ It is not yet suitable for clients that require:
|
|||||||
|
|
||||||
- AtomPub,
|
- AtomPub,
|
||||||
- SOAP/Web Services,
|
- SOAP/Web Services,
|
||||||
|
- `getDescendants` or `getFolderTree`,
|
||||||
- full CMIS SQL,
|
- full CMIS SQL,
|
||||||
- mutating multifiling/unfiling,
|
- mutating multifiling/unfiling,
|
||||||
- private-working-copy semantics,
|
- private-working-copy semantics,
|
||||||
@@ -111,7 +115,7 @@ It is not yet suitable for clients that require:
|
|||||||
- rendition streams,
|
- rendition streams,
|
||||||
- bulk update properties,
|
- bulk update properties,
|
||||||
- apply/remove policy,
|
- apply/remove policy,
|
||||||
- strict byte-stream download semantics instead of content stream descriptors.
|
- standard CMIS property mutation beyond `kontextual:metadata:<key>`.
|
||||||
|
|
||||||
## Optional OpenCMIS TCK
|
## Optional OpenCMIS TCK
|
||||||
|
|
||||||
@@ -134,13 +138,15 @@ capability groups before treating them as implementation bugs.
|
|||||||
## Operational Notes
|
## Operational Notes
|
||||||
|
|
||||||
- Hidden objects should be treated as not found by CMIS clients.
|
- Hidden objects should be treated as not found by CMIS clients.
|
||||||
- Multifiling is projection-only: assets may appear under multiple derived
|
- Multiple parent folders are projection-only: assets may appear under several
|
||||||
folder paths without changing canonical asset identity.
|
derived folder paths without changing canonical asset identity. The standard
|
||||||
|
CMIS multifiling capability is advertised as unsupported because no
|
||||||
|
add/remove filing mutation service is exposed.
|
||||||
- Relationship and change-log responses are filtered through the same visibility
|
- Relationship and change-log responses are filtered through the same visibility
|
||||||
gates as object reads.
|
gates as object reads.
|
||||||
- Mutations always pass through engine services and produce normal engine audit
|
- Mutations always pass through engine services and produce normal engine audit
|
||||||
events.
|
events.
|
||||||
- Delete is currently a governed lifecycle transition to `delete_requested`,
|
- Delete is a governed lifecycle transition to `delete_requested`; after the
|
||||||
not physical removal.
|
transition the object is no longer exposed through CMIS reads.
|
||||||
- Compatibility should be discussed per profile and per client rather than as a
|
- Compatibility should be discussed per profile and per client rather than as a
|
||||||
repo-wide binary property.
|
repo-wide binary property.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Date: 2026-05-06
|
Date: 2026-05-06
|
||||||
|
|
||||||
Status: Browser Binding MVP implemented.
|
Status: Browser Binding subset implemented and capability-hardened.
|
||||||
|
|
||||||
## Implemented Slice
|
## Implemented Slice
|
||||||
|
|
||||||
@@ -61,6 +61,11 @@ model.
|
|||||||
- relationship primitives as CMIS relationship objects,
|
- relationship primitives as CMIS relationship objects,
|
||||||
- profile-derived allowable actions.
|
- profile-derived allowable actions.
|
||||||
|
|
||||||
|
Repository info now uses conservative standard CMIS flags: optional services we
|
||||||
|
do not implement are advertised as `false` or `none`, while Kontextual-specific
|
||||||
|
projection behavior is exposed through repository feature metadata and an
|
||||||
|
unsupported-feature catalog.
|
||||||
|
|
||||||
The mapper returns `None` for assets or relationships that the access-point
|
The mapper returns `None` for assets or relationships that the access-point
|
||||||
profile must not expose. It does not fetch from repositories directly; callers
|
profile must not expose. It does not fetch from repositories directly; callers
|
||||||
provide the asset, representations, versions, metadata records, and
|
provide the asset, representations, versions, metadata records, and
|
||||||
@@ -105,6 +110,10 @@ These routes delegate to existing engine services:
|
|||||||
Read-only profiles reject the same mutations with CMIS-shaped authorization
|
Read-only profiles reject the same mutations with CMIS-shaped authorization
|
||||||
diagnostics before touching engine services.
|
diagnostics before touching engine services.
|
||||||
|
|
||||||
|
The authoring slice intentionally supports only `kontextual:metadata:<key>`
|
||||||
|
property updates. Attempts to update standard `cmis:*` properties return
|
||||||
|
structured validation diagnostics instead of being silently ignored.
|
||||||
|
|
||||||
## ACL And Redaction Slice
|
## ACL And Redaction Slice
|
||||||
|
|
||||||
The Browser Binding adapter now projects profile-derived ACLs through
|
The Browser Binding adapter now projects profile-derived ACLs through
|
||||||
@@ -122,7 +131,7 @@ asset IDs through relationship targets or audit-backed change entries.
|
|||||||
|
|
||||||
## Projection-Only Multifiling
|
## Projection-Only Multifiling
|
||||||
|
|
||||||
CMIS navigation now supports projection-only multifiling. The same asset can be
|
CMIS navigation now supports projection-only parent maps. The same asset can be
|
||||||
listed under several derived folder paths, including source system, topics,
|
listed under several derived folder paths, including source system, topics,
|
||||||
owner, lifecycle, and asset type. These folders are navigation projections; they
|
owner, lifecycle, and asset type. These folders are navigation projections; they
|
||||||
do not duplicate assets and do not become canonical storage locations.
|
do not duplicate assets and do not become canonical storage locations.
|
||||||
@@ -131,6 +140,10 @@ do not duplicate assets and do not become canonical storage locations.
|
|||||||
parent folders for one asset. `GET /cmis/{access_point_id}/browser/children`
|
parent folders for one asset. `GET /cmis/{access_point_id}/browser/children`
|
||||||
supports folder-scoped navigation through those projected paths.
|
supports folder-scoped navigation through those projected paths.
|
||||||
|
|
||||||
|
The standard CMIS `capabilityMultifiling` flag remains `false` because the
|
||||||
|
engine does not expose mutating filing services such as `addObjectToFolder` or
|
||||||
|
`removeObjectFromFolder`.
|
||||||
|
|
||||||
## Fixture And Optional TCK Integration
|
## Fixture And Optional TCK Integration
|
||||||
|
|
||||||
CMIS fixtures now act as active compatibility contracts:
|
CMIS fixtures now act as active compatibility contracts:
|
||||||
|
|||||||
@@ -103,8 +103,8 @@
|
|||||||
"service": "navigation",
|
"service": "navigation",
|
||||||
"examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"],
|
"examples": ["root-folder", "topic-folder", "source-system-folder", "unfiled-assets"],
|
||||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||||
"must_validate": ["get_children", "get_descendants", "get_folder_tree", "get_object_by_path"],
|
"must_validate": ["get_children", "projection_parent_folders", "unsupported_descendants_flag", "unsupported_folder_tree_flag"],
|
||||||
"unsupported": ["unfiling"]
|
"unsupported": ["get_descendants", "get_folder_tree", "multifiling", "unfiling"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "object-content",
|
"id": "object-content",
|
||||||
@@ -119,8 +119,8 @@
|
|||||||
"service": "versioning",
|
"service": "versioning",
|
||||||
"examples": ["single-version-document", "multi-version-document", "read-only-checkout"],
|
"examples": ["single-version-document", "multi-version-document", "read-only-checkout"],
|
||||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||||
"must_validate": ["version_series", "latest_version_flags", "version_history"],
|
"must_validate": ["version_properties_projection", "latest_version_flags", "unsupported_versioning_flags"],
|
||||||
"unsupported": ["private_working_copy"]
|
"unsupported": ["versioning_services", "private_working_copy", "all_versions_search"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "discovery-query",
|
"id": "discovery-query",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
"examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "unsupported-join"],
|
"examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "unsupported-join"],
|
||||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||||
"must_validate": ["query_capabilities", "paging", "unsupported_grammar_diagnostics"],
|
"must_validate": ["query_capabilities", "paging", "unsupported_grammar_diagnostics"],
|
||||||
"unsupported": ["full_cmis_sql_joins"]
|
"unsupported": ["full_cmis_sql_joins", "order_by"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "relationships",
|
"id": "relationships",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"examples": ["public-asset", "internal-asset", "confidential-asset", "denied-actor", "service-account"],
|
"examples": ["public-asset", "internal-asset", "confidential-asset", "denied-actor", "service-account"],
|
||||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||||
"must_validate": ["allowable_actions", "acl_projection", "denial_omits_object", "no_metadata_leakage"],
|
"must_validate": ["allowable_actions", "acl_projection", "denial_omits_object", "no_metadata_leakage"],
|
||||||
"unsupported": ["apply_policy", "remove_policy"]
|
"unsupported": ["apply_acl", "apply_policy", "remove_policy"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "change-log",
|
"id": "change-log",
|
||||||
@@ -166,16 +166,23 @@
|
|||||||
"unsupported_diagnostics": {
|
"unsupported_diagnostics": {
|
||||||
"atompub": "binding_not_supported",
|
"atompub": "binding_not_supported",
|
||||||
"web_services": "binding_not_supported",
|
"web_services": "binding_not_supported",
|
||||||
|
"get_descendants": "capability_not_supported",
|
||||||
|
"get_folder_tree": "capability_not_supported",
|
||||||
"multifiling": "projection_only",
|
"multifiling": "projection_only",
|
||||||
"unfiling": "capability_not_supported",
|
"unfiling": "capability_not_supported",
|
||||||
"append_content_stream": "capability_not_supported",
|
"append_content_stream": "capability_not_supported",
|
||||||
|
"versioning_services": "capability_not_supported",
|
||||||
"private_working_copy": "capability_not_supported",
|
"private_working_copy": "capability_not_supported",
|
||||||
|
"all_versions_search": "capability_not_supported",
|
||||||
"full_cmis_sql_joins": "query_not_supported",
|
"full_cmis_sql_joins": "query_not_supported",
|
||||||
|
"order_by": "query_not_supported",
|
||||||
|
"apply_acl": "operation_not_implemented",
|
||||||
"apply_policy": "capability_not_supported",
|
"apply_policy": "capability_not_supported",
|
||||||
"remove_policy": "capability_not_supported",
|
"remove_policy": "capability_not_supported",
|
||||||
"retention_hold_mutation": "capability_not_supported",
|
"retention_hold_mutation": "capability_not_supported",
|
||||||
"bulk_update_properties": "capability_not_supported",
|
"bulk_update_properties": "capability_not_supported",
|
||||||
"rendition_streams": "capability_not_supported"
|
"rendition_streams": "capability_not_supported",
|
||||||
|
"type_mutability": "capability_not_supported"
|
||||||
},
|
},
|
||||||
"profile_expectations": {
|
"profile_expectations": {
|
||||||
"readonly-browser": {
|
"readonly-browser": {
|
||||||
|
|||||||
@@ -534,7 +534,14 @@ class ServiceRuntime:
|
|||||||
expected = properties.pop("expected_current_version_id", payload.get("expected_current_version_id", None))
|
expected = properties.pop("expected_current_version_id", payload.get("expected_current_version_id", None))
|
||||||
for key, value in properties.items():
|
for key, value in properties.items():
|
||||||
if key.startswith("cmis:"):
|
if key.startswith("cmis:"):
|
||||||
continue
|
raise ValidationError(
|
||||||
|
"Unsupported CMIS property update",
|
||||||
|
details={
|
||||||
|
"property": key,
|
||||||
|
"operation": "updateProperties",
|
||||||
|
"supported": ["kontextual:metadata:<key>"],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.asset_service().add_metadata_record(
|
self.asset_service().add_metadata_record(
|
||||||
asset_id,
|
asset_id,
|
||||||
MetadataRecord(key=_cmis_metadata_key(key), value=value, confirmed=bool(payload.get("confirmed", True))),
|
MetadataRecord(key=_cmis_metadata_key(key), value=value, confirmed=bool(payload.get("confirmed", True))),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
|
|
||||||
from .actors import ActorType, OperationContext
|
from .actors import ActorType, OperationContext
|
||||||
from .assets import AssetRepresentation, KnowledgeAsset, RepresentationKind
|
from .assets import AssetRepresentation, KnowledgeAsset, RepresentationKind
|
||||||
from .metadata import Sensitivity
|
from .metadata import LifecycleState, Sensitivity
|
||||||
from .policy import PolicyDecision
|
from .policy import PolicyDecision
|
||||||
from .provenance import AssetVersion
|
from .provenance import AssetVersion
|
||||||
from .relationships import CoreRelationship, RelationshipTargetKind
|
from .relationships import CoreRelationship, RelationshipTargetKind
|
||||||
@@ -92,6 +92,24 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
|||||||
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
|
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
|
||||||
CMISAction.BULK_UPDATE_PROPERTIES: CMISCapability.BULK_UPDATE,
|
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 = {
|
MUTATION_ACTIONS = {
|
||||||
CMISAction.CREATE_DOCUMENT,
|
CMISAction.CREATE_DOCUMENT,
|
||||||
CMISAction.UPDATE_PROPERTIES,
|
CMISAction.UPDATE_PROPERTIES,
|
||||||
@@ -101,6 +119,90 @@ MUTATION_ACTIONS = {
|
|||||||
CMISAction.APPLY_POLICY,
|
CMISAction.APPLY_POLICY,
|
||||||
CMISAction.BULK_UPDATE_PROPERTIES,
|
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)
|
@dataclass(frozen=True)
|
||||||
@@ -170,11 +272,7 @@ class CMISAccessProfile:
|
|||||||
def admin_export(cls) -> "CMISAccessProfile":
|
def admin_export(cls) -> "CMISAccessProfile":
|
||||||
return cls(
|
return cls(
|
||||||
name="admin-export",
|
name="admin-export",
|
||||||
capabilities=_read_capabilities()
|
capabilities=_read_capabilities(),
|
||||||
+ (
|
|
||||||
CMISCapability.RENDITIONS,
|
|
||||||
CMISCapability.RETENTION_HOLD,
|
|
||||||
),
|
|
||||||
allow_mutations=False,
|
allow_mutations=False,
|
||||||
visible_sensitivities=(
|
visible_sensitivities=(
|
||||||
Sensitivity.PUBLIC,
|
Sensitivity.PUBLIC,
|
||||||
@@ -203,13 +301,28 @@ class CMISAccessProfile:
|
|||||||
return CMISCapability(capability) in self.capabilities
|
return CMISCapability(capability) in self.capabilities
|
||||||
|
|
||||||
def allows_action(self, action: CMISAction | str) -> bool:
|
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)
|
cmis_action = CMISAction(action)
|
||||||
capability = ACTION_CAPABILITIES[cmis_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:
|
if cmis_action in MUTATION_ACTIONS and not self.allow_mutations:
|
||||||
return False
|
return (
|
||||||
return True
|
"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(
|
def decide_action(
|
||||||
self,
|
self,
|
||||||
@@ -234,14 +347,16 @@ class CMISAccessProfile:
|
|||||||
resource,
|
resource,
|
||||||
context={"profile": self.name},
|
context={"profile": self.name},
|
||||||
)
|
)
|
||||||
capability = ACTION_CAPABILITIES[cmis_action]
|
reason, denial_context = self.action_denial(cmis_action) or (
|
||||||
reason = "cmis_mutation_not_allowed" if cmis_action in MUTATION_ACTIONS else "cmis_capability_not_supported"
|
"cmis_capability_not_supported",
|
||||||
|
{"profile": self.name},
|
||||||
|
)
|
||||||
return PolicyDecision.deny(
|
return PolicyDecision.deny(
|
||||||
context.actor.id,
|
context.actor.id,
|
||||||
cmis_action.value,
|
cmis_action.value,
|
||||||
resource,
|
resource,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
context={"profile": self.name, "capability": capability.value},
|
context=denial_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
def allows_actor(self, context: OperationContext) -> bool:
|
def allows_actor(self, context: OperationContext) -> bool:
|
||||||
@@ -265,6 +380,14 @@ class CMISAccessProfile:
|
|||||||
context={"profile": self.name},
|
context={"profile": self.name},
|
||||||
)
|
)
|
||||||
classification = asset.classification
|
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:
|
if classification.sensitivity in self.denied_sensitivities:
|
||||||
return PolicyDecision.deny(
|
return PolicyDecision.deny(
|
||||||
context.actor.id,
|
context.actor.id,
|
||||||
@@ -466,13 +589,27 @@ class CMISDomainMapper:
|
|||||||
return {
|
return {
|
||||||
"repository_id": self.access_point.repository_id,
|
"repository_id": self.access_point.repository_id,
|
||||||
"repository_name": self.access_point.metadata.get("repository_name", 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",
|
"cmis_version_supported": "1.1",
|
||||||
"root_folder_id": self.access_point.root_folder_id,
|
"root_folder_id": self.access_point.root_folder_id,
|
||||||
"principal_anonymous": "anonymous",
|
"principal_anonymous": "anonymous",
|
||||||
"principal_anyone": "anyone",
|
"principal_anyone": "anyone",
|
||||||
|
"vendor_name": "Kontextual",
|
||||||
"product_name": "kontextual-engine",
|
"product_name": "kontextual-engine",
|
||||||
|
"product_version": self.access_point.metadata.get("product_version", "0.1.0"),
|
||||||
"binding": profile.binding.value,
|
"binding": profile.binding.value,
|
||||||
"capabilities": self.capability_flags(),
|
"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,
|
"profile": profile.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,27 +617,53 @@ class CMISDomainMapper:
|
|||||||
profile = self.access_point.profile
|
profile = self.access_point.profile
|
||||||
return {
|
return {
|
||||||
"capability_content_stream_updatability": (
|
"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"
|
"capability_changes": "objectidsonly"
|
||||||
if profile.has_capability(CMISCapability.CHANGE_LOG)
|
if profile.has_capability(CMISCapability.CHANGE_LOG)
|
||||||
else "none",
|
else "none",
|
||||||
"capability_renditions": "read" if profile.has_capability(CMISCapability.RENDITIONS) else "none",
|
"capability_renditions": "none",
|
||||||
"capability_get_descendants": profile.has_capability(CMISCapability.NAVIGATION),
|
"capability_get_descendants": False,
|
||||||
"capability_get_folder_tree": profile.has_capability(CMISCapability.NAVIGATION),
|
"capability_get_folder_tree": False,
|
||||||
"capability_multifiling": profile.has_capability(CMISCapability.NAVIGATION),
|
"capability_order_by": "none",
|
||||||
|
"capability_multifiling": False,
|
||||||
"capability_unfiling": False,
|
"capability_unfiling": False,
|
||||||
"capability_version_specific_filing": False,
|
"capability_version_specific_filing": False,
|
||||||
"capability_pwc_searchable": False,
|
"capability_pwc_searchable": False,
|
||||||
"capability_pwc_updatable": False,
|
"capability_pwc_updatable": False,
|
||||||
"capability_all_versions_searchable": profile.has_capability(CMISCapability.VERSIONING),
|
"capability_all_versions_searchable": False,
|
||||||
"capability_query": "metadataonly"
|
"capability_query": "metadataonly"
|
||||||
if profile.has_capability(CMISCapability.DISCOVERY_QUERY)
|
if profile.has_capability(CMISCapability.DISCOVERY_QUERY)
|
||||||
else "none",
|
else "none",
|
||||||
"capability_join": "none",
|
"capability_join": "none",
|
||||||
"capability_acl": "discover" if profile.has_capability(CMISCapability.ACL) else "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]]:
|
def type_definitions(self) -> list[dict[str, Any]]:
|
||||||
can_write = self.access_point.profile.allow_mutations
|
can_write = self.access_point.profile.allow_mutations
|
||||||
return [
|
return [
|
||||||
@@ -537,7 +700,7 @@ class CMISDomainMapper:
|
|||||||
type_id=f"kontextual:{asset.classification.asset_type}",
|
type_id=f"kontextual:{asset.classification.asset_type}",
|
||||||
name=asset.title,
|
name=asset.title,
|
||||||
path=self.asset_path(asset),
|
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),
|
allowable_actions=self.allowable_actions(context, has_content_stream=content_stream is not None),
|
||||||
content_stream=content_stream,
|
content_stream=content_stream,
|
||||||
version=self.version_properties(asset, current_version, versions),
|
version=self.version_properties(asset, current_version, versions),
|
||||||
@@ -682,6 +845,7 @@ class CMISDomainMapper:
|
|||||||
asset: KnowledgeAsset,
|
asset: KnowledgeAsset,
|
||||||
*,
|
*,
|
||||||
metadata_records: list[Any] | tuple[Any, ...] = (),
|
metadata_records: list[Any] | tuple[Any, ...] = (),
|
||||||
|
content_stream: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
classification = asset.classification
|
classification = asset.classification
|
||||||
properties = {
|
properties = {
|
||||||
@@ -694,6 +858,10 @@ class CMISDomainMapper:
|
|||||||
"cmis:creationDate": asset.created_at,
|
"cmis:creationDate": asset.created_at,
|
||||||
"cmis:lastModificationDate": asset.updated_at,
|
"cmis:lastModificationDate": asset.updated_at,
|
||||||
"cmis:changeToken": asset.current_version_id,
|
"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:assetId": asset.id,
|
||||||
"kontextual:assetType": classification.asset_type,
|
"kontextual:assetType": classification.asset_type,
|
||||||
"kontextual:sensitivity": _enum_value(classification.sensitivity),
|
"kontextual:sensitivity": _enum_value(classification.sensitivity),
|
||||||
@@ -785,7 +953,6 @@ def _read_capabilities() -> tuple[CMISCapability, ...]:
|
|||||||
CMISCapability.NAVIGATION,
|
CMISCapability.NAVIGATION,
|
||||||
CMISCapability.OBJECT_READ,
|
CMISCapability.OBJECT_READ,
|
||||||
CMISCapability.CONTENT_STREAM_READ,
|
CMISCapability.CONTENT_STREAM_READ,
|
||||||
CMISCapability.VERSIONING,
|
|
||||||
CMISCapability.DISCOVERY_QUERY,
|
CMISCapability.DISCOVERY_QUERY,
|
||||||
CMISCapability.RELATIONSHIPS,
|
CMISCapability.RELATIONSHIPS,
|
||||||
CMISCapability.ACL,
|
CMISCapability.ACL,
|
||||||
@@ -803,19 +970,20 @@ def _type_definition(
|
|||||||
display_name: str,
|
display_name: str,
|
||||||
can_write: bool,
|
can_write: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
is_document = base_type_id == CMISBaseType.DOCUMENT
|
||||||
return {
|
return {
|
||||||
"id": type_id,
|
"id": type_id,
|
||||||
"local_name": type_id.split(":", 1)[-1],
|
"local_name": type_id.split(":", 1)[-1],
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"base_type_id": base_type_id.value,
|
"base_type_id": base_type_id.value,
|
||||||
"queryable": True,
|
"queryable": is_document,
|
||||||
"controllable_acl": base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
|
"controllable_acl": is_document,
|
||||||
"controllable_policy": False,
|
"controllable_policy": False,
|
||||||
"creatable": can_write and base_type_id == CMISBaseType.DOCUMENT,
|
"creatable": can_write and is_document,
|
||||||
"fileable": base_type_id == CMISBaseType.DOCUMENT,
|
"fileable": is_document,
|
||||||
"fulltext_indexed": False,
|
"fulltext_indexed": False,
|
||||||
"included_in_supertype_query": True,
|
"included_in_supertype_query": is_document,
|
||||||
"versionable": base_type_id == CMISBaseType.DOCUMENT,
|
"versionable": False,
|
||||||
"property_definitions": _property_definitions(base_type_id),
|
"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:sensitivity": {"property_type": "string", "cardinality": "single", "required": False},
|
||||||
"kontextual:lifecycle": {"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:
|
if base_type_id == CMISBaseType.RELATIONSHIP:
|
||||||
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||||
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
{
|
{
|
||||||
"capability_group": "navigation",
|
"capability_group": "navigation",
|
||||||
"tck_groups": ["NavigationTestGroup"],
|
"tck_groups": ["NavigationTestGroup"],
|
||||||
"expected": "selected-pass"
|
"expected": "partial-pass",
|
||||||
|
"known_gaps": ["get_descendants", "get_folder_tree", "multifiling", "unfiling"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_group": "object-content",
|
"capability_group": "object-content",
|
||||||
@@ -30,14 +31,14 @@
|
|||||||
{
|
{
|
||||||
"capability_group": "versioning",
|
"capability_group": "versioning",
|
||||||
"tck_groups": ["VersioningTestGroup"],
|
"tck_groups": ["VersioningTestGroup"],
|
||||||
"expected": "partial-pass",
|
"expected": "skip",
|
||||||
"known_gaps": ["private_working_copy"]
|
"known_gaps": ["versioning_services", "private_working_copy", "all_versions_search"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_group": "discovery-query",
|
"capability_group": "discovery-query",
|
||||||
"tck_groups": ["QueryTestGroup"],
|
"tck_groups": ["QueryTestGroup"],
|
||||||
"expected": "partial-pass",
|
"expected": "partial-pass",
|
||||||
"known_gaps": ["full_cmis_sql_joins"]
|
"known_gaps": ["full_cmis_sql_joins", "order_by"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_group": "relationships",
|
"capability_group": "relationships",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"capability_group": "acl-policy",
|
"capability_group": "acl-policy",
|
||||||
"tck_groups": ["AclTestGroup", "PolicyTestGroup"],
|
"tck_groups": ["AclTestGroup", "PolicyTestGroup"],
|
||||||
"expected": "partial-pass",
|
"expected": "partial-pass",
|
||||||
"known_gaps": ["apply_policy", "remove_policy"]
|
"known_gaps": ["apply_acl", "apply_policy", "remove_policy"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_group": "change-log",
|
"capability_group": "change-log",
|
||||||
@@ -63,4 +64,3 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ def test_governed_authoring_profile_allows_selected_write_actions() -> None:
|
|||||||
assert profile.decide_action(CMISAction.CREATE_DOCUMENT, context).allowed is True
|
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.SET_CONTENT_STREAM, context).allowed is True
|
||||||
assert profile.decide_action(CMISAction.BULK_UPDATE_PROPERTIES, context).allowed is False
|
assert profile.decide_action(CMISAction.BULK_UPDATE_PROPERTIES, context).allowed is False
|
||||||
|
assert profile.decide_action(CMISAction.APPLY_ACL, context).reason == "cmis_operation_not_implemented"
|
||||||
|
|
||||||
|
|
||||||
def test_profiles_hide_denied_sensitivities_without_partial_exposure() -> None:
|
def test_profiles_hide_denied_sensitivities_without_partial_exposure() -> None:
|
||||||
@@ -148,4 +149,3 @@ def test_disabled_access_point_denies_all_actions_and_visibility() -> None:
|
|||||||
|
|
||||||
assert access_point.decide_action(CMISAction.GET_OBJECT, context).reason == "cmis_access_point_disabled"
|
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
|
assert access_point.exposes_asset(_asset("asset-public", "public"), context) is False
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
|||||||
assert repository["repository_id"] == "kontextual-readonly-browser"
|
assert repository["repository_id"] == "kontextual-readonly-browser"
|
||||||
assert repository["cmis_version_supported"] == "1.1"
|
assert repository["cmis_version_supported"] == "1.1"
|
||||||
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
||||||
|
assert repository["capabilities"]["capability_get_descendants"] is False
|
||||||
|
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only"
|
||||||
assert {item["base_type_id"] for item in types["items"]} >= {
|
assert {item["base_type_id"] for item in types["items"]} >= {
|
||||||
"cmis:document",
|
"cmis:document",
|
||||||
"cmis:folder",
|
"cmis:folder",
|
||||||
@@ -208,3 +210,14 @@ def test_cmis_readonly_route_rejects_mutation(cmis_client) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmis_rejects_unsupported_standard_property_update_with_diagnostics(cmis_client) -> None:
|
||||||
|
response = cmis_client.post(
|
||||||
|
"/cmis/governed-authoring/browser/object/cmis:asset:asset-source/properties",
|
||||||
|
json={"properties": {"cmis:name": "Renamed"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
assert response.json()["detail"]["details"]["property"] == "cmis:name"
|
||||||
|
assert response.json()["detail"]["details"]["supported"] == ["kontextual:metadata:<key>"]
|
||||||
|
|||||||
145
tests/cmis/test_cmis_compliance_flags.py
Normal file
145
tests/cmis/test_cmis_compliance_flags.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from kontextual_engine import (
|
||||||
|
Actor,
|
||||||
|
ActorType,
|
||||||
|
CMISAccessPoint,
|
||||||
|
CMISAccessProfile,
|
||||||
|
CMISAction,
|
||||||
|
CMISBaseType,
|
||||||
|
CMISCapability,
|
||||||
|
CMISDomainMapper,
|
||||||
|
Classification,
|
||||||
|
KnowledgeAsset,
|
||||||
|
LifecycleState,
|
||||||
|
OperationContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_CAPABILITY_KEYS = {
|
||||||
|
"capability_content_stream_updatability",
|
||||||
|
"capability_changes",
|
||||||
|
"capability_renditions",
|
||||||
|
"capability_get_descendants",
|
||||||
|
"capability_get_folder_tree",
|
||||||
|
"capability_order_by",
|
||||||
|
"capability_multifiling",
|
||||||
|
"capability_unfiling",
|
||||||
|
"capability_version_specific_filing",
|
||||||
|
"capability_pwc_searchable",
|
||||||
|
"capability_pwc_updatable",
|
||||||
|
"capability_all_versions_searchable",
|
||||||
|
"capability_query",
|
||||||
|
"capability_join",
|
||||||
|
"capability_acl",
|
||||||
|
"capability_new_type_settable_attributes",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _context() -> OperationContext:
|
||||||
|
return OperationContext.create(
|
||||||
|
Actor.create(ActorType.HUMAN, actor_id="actor-cmis-compliance"),
|
||||||
|
correlation_id="corr-cmis-compliance",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mapper(profile: CMISAccessProfile | None = None) -> CMISDomainMapper:
|
||||||
|
return CMISDomainMapper(
|
||||||
|
CMISAccessPoint(
|
||||||
|
access_point_id="cmis-compliance",
|
||||||
|
repository_id="kontextual-compliance",
|
||||||
|
profile=profile or CMISAccessProfile.readonly_browser(),
|
||||||
|
base_path="/cmis/compliance/browser",
|
||||||
|
metadata={"repository_name": "Kontextual Compliance Repository"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_repository_info_uses_complete_conservative_cmis_11_capability_flags() -> None:
|
||||||
|
repository = _mapper().repository_info()
|
||||||
|
capabilities = repository["capabilities"]
|
||||||
|
|
||||||
|
assert EXPECTED_CAPABILITY_KEYS <= set(capabilities)
|
||||||
|
assert capabilities["capability_content_stream_updatability"] == "none"
|
||||||
|
assert capabilities["capability_changes"] == "objectidsonly"
|
||||||
|
assert capabilities["capability_renditions"] == "none"
|
||||||
|
assert capabilities["capability_get_descendants"] is False
|
||||||
|
assert capabilities["capability_get_folder_tree"] is False
|
||||||
|
assert capabilities["capability_order_by"] == "none"
|
||||||
|
assert capabilities["capability_multifiling"] is False
|
||||||
|
assert capabilities["capability_unfiling"] is False
|
||||||
|
assert capabilities["capability_version_specific_filing"] is False
|
||||||
|
assert capabilities["capability_pwc_searchable"] is False
|
||||||
|
assert capabilities["capability_pwc_updatable"] is False
|
||||||
|
assert capabilities["capability_all_versions_searchable"] is False
|
||||||
|
assert capabilities["capability_query"] == "metadataonly"
|
||||||
|
assert capabilities["capability_join"] == "none"
|
||||||
|
assert capabilities["capability_acl"] == "discover"
|
||||||
|
assert set(capabilities["capability_new_type_settable_attributes"].values()) == {False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_authoring_profile_only_advertises_supported_content_write_semantics() -> None:
|
||||||
|
repository = _mapper(CMISAccessProfile.governed_authoring()).repository_info()
|
||||||
|
capabilities = repository["capabilities"]
|
||||||
|
|
||||||
|
assert capabilities["capability_content_stream_updatability"] == "anytime"
|
||||||
|
assert capabilities["capability_multifiling"] is False
|
||||||
|
assert capabilities["capability_renditions"] == "none"
|
||||||
|
|
||||||
|
|
||||||
|
def test_repository_info_exposes_nonstandard_projection_feature_and_unsupported_catalog() -> None:
|
||||||
|
repository = _mapper().repository_info()
|
||||||
|
feature_ids = {feature["id"] for feature in repository["repository_features"]}
|
||||||
|
unsupported = repository["unsupported_features"]
|
||||||
|
|
||||||
|
assert "urn:kontextual:cmis:feature:projection-parentage" in feature_ids
|
||||||
|
assert unsupported["multifiling"]["status"] == "projection_only"
|
||||||
|
assert unsupported["multifiling"]["standard_flag"] == "capability_multifiling"
|
||||||
|
assert unsupported["get_descendants"]["standard_flag"] == "capability_get_descendants"
|
||||||
|
assert unsupported["rendition_streams"]["standard_flag"] == "capability_renditions"
|
||||||
|
assert repository["compliance"]["posture"] == "declared-browser-binding-subset"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_definitions_do_not_claim_unimplemented_cmis_services() -> None:
|
||||||
|
types = {definition["base_type_id"]: definition for definition in _mapper().type_definitions()}
|
||||||
|
document = types[CMISBaseType.DOCUMENT.value]
|
||||||
|
folder = types[CMISBaseType.FOLDER.value]
|
||||||
|
relationship = types[CMISBaseType.RELATIONSHIP.value]
|
||||||
|
policy = types[CMISBaseType.POLICY.value]
|
||||||
|
|
||||||
|
assert document["versionable"] is False
|
||||||
|
assert document["controllable_acl"] is True
|
||||||
|
assert document["queryable"] is True
|
||||||
|
assert document["creatable"] is False
|
||||||
|
assert "cmis:contentStreamLength" in document["property_definitions"]
|
||||||
|
assert folder["controllable_acl"] is False
|
||||||
|
assert folder["queryable"] is False
|
||||||
|
assert relationship["creatable"] is False
|
||||||
|
assert relationship["queryable"] is False
|
||||||
|
assert policy["controllable_policy"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_denies_unimplemented_mutations_even_when_related_capability_exists() -> None:
|
||||||
|
profile = CMISAccessProfile(
|
||||||
|
name="acl-manager-shape",
|
||||||
|
capabilities=(CMISCapability.ACL,),
|
||||||
|
allow_mutations=True,
|
||||||
|
)
|
||||||
|
decision = profile.decide_action(CMISAction.APPLY_ACL, _context())
|
||||||
|
|
||||||
|
assert decision.allowed is False
|
||||||
|
assert decision.reason == "cmis_operation_not_implemented"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deleted_assets_are_not_exposed_as_live_cmis_objects() -> None:
|
||||||
|
profile = CMISAccessProfile.readonly_browser()
|
||||||
|
asset = KnowledgeAsset.create(
|
||||||
|
"Deleted",
|
||||||
|
Classification(asset_type="document", sensitivity="internal"),
|
||||||
|
asset_id="asset-deleted",
|
||||||
|
).transition_lifecycle(LifecycleState.DELETE_REQUESTED)
|
||||||
|
|
||||||
|
decision = profile.decide_asset_visibility(asset, _context())
|
||||||
|
|
||||||
|
assert decision.allowed is False
|
||||||
|
assert decision.reason == "cmis_lifecycle_not_visible"
|
||||||
@@ -73,7 +73,8 @@ def test_mapper_exposes_repository_info_capabilities_and_base_type_definitions()
|
|||||||
assert repository["cmis_version_supported"] == "1.1"
|
assert repository["cmis_version_supported"] == "1.1"
|
||||||
assert repository["binding"] == "browser"
|
assert repository["binding"] == "browser"
|
||||||
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
||||||
assert repository["capabilities"]["capability_multifiling"] is True
|
assert repository["capabilities"]["capability_multifiling"] is False
|
||||||
|
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only"
|
||||||
|
|
||||||
assert set(types) == {
|
assert set(types) == {
|
||||||
"cmis:document",
|
"cmis:document",
|
||||||
@@ -122,6 +123,8 @@ def test_mapper_projects_asset_to_cmis_document_envelope() -> None:
|
|||||||
assert serialized["properties"]["cmis:objectTypeId"] == "kontextual:document"
|
assert serialized["properties"]["cmis:objectTypeId"] == "kontextual:document"
|
||||||
assert serialized["properties"]["kontextual:metadata:status"] == "accepted"
|
assert serialized["properties"]["kontextual:metadata:status"] == "accepted"
|
||||||
assert serialized["content_stream"]["mime_type"] == "text/markdown"
|
assert serialized["content_stream"]["mime_type"] == "text/markdown"
|
||||||
|
assert serialized["properties"]["cmis:contentStreamMimeType"] == "text/markdown"
|
||||||
|
assert serialized["properties"]["cmis:contentStreamId"] == "repr-source"
|
||||||
assert serialized["version"]["cmis:versionLabel"] == "3"
|
assert serialized["version"]["cmis:versionLabel"] == "3"
|
||||||
assert serialized["relationships"] == ["cmis:relationship:rel-derived"]
|
assert serialized["relationships"] == ["cmis:relationship:rel-derived"]
|
||||||
assert CMISAction.GET_CONTENT_STREAM.value in serialized["allowable_actions"]
|
assert CMISAction.GET_CONTENT_STREAM.value in serialized["allowable_actions"]
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ def test_runtime_cmis_browser_repository_types_children_and_object(cmis_runtime)
|
|||||||
|
|
||||||
assert access_points["count"] == 4
|
assert access_points["count"] == 4
|
||||||
assert repository["repository_id"] == "kontextual-readonly-browser"
|
assert repository["repository_id"] == "kontextual-readonly-browser"
|
||||||
assert repository["capabilities"]["capability_get_descendants"] is True
|
assert repository["capabilities"]["capability_get_descendants"] is False
|
||||||
|
assert repository["capabilities"]["capability_get_folder_tree"] is False
|
||||||
|
assert repository["unsupported_features"]["get_descendants"]["status"] == "unsupported"
|
||||||
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
|
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
|
||||||
root_paths = {item["path"] for item in children["objects"]}
|
root_paths = {item["path"] for item in children["objects"]}
|
||||||
topic_object_ids = {item["object_id"] for item in topic_children["objects"]}
|
topic_object_ids = {item["object_id"] for item in topic_children["objects"]}
|
||||||
@@ -183,6 +185,23 @@ def test_runtime_cmis_governed_authoring_allows_selected_mutations(cmis_runtime)
|
|||||||
assert stream_bytes.representation.storage_ref.startswith("blob://memory/")
|
assert stream_bytes.representation.storage_ref.startswith("blob://memory/")
|
||||||
assert deleted["deleted"] is False
|
assert deleted["deleted"] is False
|
||||||
assert deleted["lifecycle"] == "delete_requested"
|
assert deleted["lifecycle"] == "delete_requested"
|
||||||
|
with pytest.raises(Exception) as exc_info:
|
||||||
|
runtime.cmis_object("governed-authoring", "cmis:asset:asset-authored", context)
|
||||||
|
assert "CMIS object not found" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime) -> None:
|
||||||
|
runtime, context = cmis_runtime
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as exc_info:
|
||||||
|
runtime.cmis_update_properties(
|
||||||
|
"governed-authoring",
|
||||||
|
"cmis:asset:asset-runtime-source",
|
||||||
|
{"properties": {"cmis:name": "Renamed"}},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Unsupported CMIS property update" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_cmis_readonly_profile_rejects_mutations(cmis_runtime) -> None:
|
def test_runtime_cmis_readonly_profile_rejects_mutations(cmis_runtime) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user