diff --git a/docs/access-control-policy-gateway.md b/docs/access-control-policy-gateway.md index 7cfe8b4..7bcf43f 100644 --- a/docs/access-control-policy-gateway.md +++ b/docs/access-control-policy-gateway.md @@ -139,6 +139,22 @@ to future backend storage. ## Adapter Boundaries +Enterprise IAM integration is covered in +`docs/enterprise-access-control-integration.md`. In that architecture, +Markitect is the policy enforcement point for Markdown knowledge results, while +NetKingdom/key-cape-compatible OIDC supplies identity claims and external +policy engines can act as policy decision points. + +Identity and directory integration use these provider-neutral boundaries: + +- `IdentityClaimsAdapter` validates OIDC/JWT/SAML material and returns + normalized `EnterpriseIdentity`. +- `DirectoryGroupResolver` resolves group overage or stale directory claims + through SCIM/Graph/LDAP/Keycloak-style adapters. +- `EnterprisePolicyMapper` maps canonical enterprise roles, scopes, and groups + to `PolicySubject` labels, trust zones, and allowed actions. +- `DecisionLogStore` persists durable audit records for policy decisions. + Relationship policies use `RelationshipPolicyAdapter`: ```text diff --git a/docs/enterprise-access-control-integration.md b/docs/enterprise-access-control-integration.md new file mode 100644 index 0000000..1c06d16 --- /dev/null +++ b/docs/enterprise-access-control-integration.md @@ -0,0 +1,335 @@ +# Enterprise Access-Control Integration + +Date: 2026-05-04 + +## Purpose + +This note explains how Markitect's access-control gateway should integrate with +enterprise IAM while respecting the local NetKingdom/key-cape direction. + +The answer is yes: trust zones and access groups can map to canonical directory +group membership, but Markitect should not trust raw AD group names directly. +The enterprise shape should be: + +```text +OIDC/SAML/SCIM identity and directory plane + -> canonical subject claims + -> policy map and/or policy decision point + -> Markitect policy gateway + -> filtered document/query/context results +``` + +Markitect is the policy enforcement point for Markdown knowledge results. It +should validate and normalize identity data through adapters, then call a policy +decision point through the existing policy interfaces. + +## Canonical Enterprise Shape + +The canonical enterprise pattern is the PEP/PDP/PIP/PAP split: + +| Component | Markitect fit | +| --- | --- | +| PEP: policy enforcement point | `AccessPolicyGateway` at query, search, context, workflow, export, and assisted-prompt boundaries. | +| PDP: policy decision point | Local label policy for development; OpenFGA/SpiceDB, OPA/Rego, Cedar, or enterprise authorization service through adapters. | +| PIP: policy information point | NetKingdom/key-cape OIDC claims, directory group resolution, object labels, backend metadata, workflow context, and environment attributes. | +| PAP: policy administration point | Enterprise IAM/policy administration, plus versioned Markitect mapping files for labels, actions, trust zones, and emergency rules. | + +This keeps Markitect small and auditable. It enforces decisions where Markdown +knowledge leaves a boundary, but it does not become the enterprise directory, +SSO provider, or policy administration system. + +## Local Infrastructure Fit + +The local canon already points the right way: + +- NetKingdom SSO is the reference identity provider. +- Keycloak is the reference OIDC provider, with privacyIDEA-backed MFA. +- key-cape is the lightweight OIDC/profile-enforcement path for local, + sandbox, and bootstrap scenarios. +- Services should trust OIDC tokens, validate issuer/audience/signature/expiry, + and authorize from explicit roles/scopes. + +Markitect should therefore consume the NetKingdom IAM profile rather than +create its own identity standard. + +## Enterprise Reference Architecture + +```mermaid +flowchart LR + AD["AD / LDAP / HRIS"] --> SCIM["SCIM / Directory Sync
(PIP)"] + SCIM --> IdP["NetKingdom SSO
Keycloak / key-cape-compatible OIDC"] + IdP --> Token["OIDC Access Token
roles, scopes, groups, org, assurance"] + Token --> Mapper["Identity Claim Mapper"] + Mapper --> Subject["PolicySubject"] + Source["Markdown objects
labels, paths, trust zones"] --> Object["PolicyObject"] + Subject --> Gateway["Markitect AccessPolicyGateway
(PEP)"] + Object --> Gateway + Gateway --> PDP["Policy Decision Point
local / OpenFGA / OPA / Cedar"] + PDP --> Decision["PolicyDecision + audit metadata"] + Decision --> Boundary["Filtered query/search/context results"] +``` + +### Identity Plane + +OIDC should be the default authentication path for users and services. SAML may +remain relevant for enterprise federation, but Markitect should normalize both +into the same subject shape. + +The canonical accepted claims should follow the NetKingdom IAM profile: + +- `iss` +- `sub` +- `aud` +- `exp` +- `iat` +- `scope` or `scp` +- `preferred_username` +- `roles` or `realm_access.roles` +- recommended `groups`, `azp`, `email`, `name` + +For humans, Authorization Code + PKCE is the right login flow. For services, +client credentials or workload identity should produce short-lived service +tokens. OAuth token exchange can support hub-to-hub delegation where a service +acts on behalf of a user. + +### Directory And Group Plane + +Enterprise directories remain authoritative for group membership. Provisioning +should use a standard such as SCIM where possible; AD/LDAP synchronization into +Keycloak is also reasonable when NetKingdom owns the identity plane. + +Important design rule: + +```text +directory groups -> canonical roles/scopes/trust labels -> Markitect subject +``` + +Avoid: + +```text +raw AD group name -> direct Markitect privilege +``` + +Reasons: + +- AD group names change and often encode organizational accidents. +- Token group claims can be too large for large organizations. +- Privilege should be expressed as app roles/scopes or an explicit mapping file. +- Group membership freshness must be visible in the decision trail. + +For Microsoft Entra-style group claims, large tenants may hit token group +overage. Markitect should therefore support a group-resolution adapter rather +than assuming all groups are always present in the token. + +### Policy Plane + +The existing Markitect interfaces already provide the main policy boundary: + +```text +AccessPolicyGateway.authorize(subject, action, object_id, context) +AccessPolicyGateway.filter_results(subject, action, results, context) +AccessPolicyGateway.explain_decision(decision_id) +``` + +The existing data models also fit enterprise needs: + +- `PolicySubject`: identity, roles, groups/labels, allowed actions, attributes. +- `PolicyObject`: path, labels, trust zone, attributes. +- `PolicyDecision`: stable decision id, effect, reason, labels, trust zone, + metadata. +- `PolicyFilterResult`: filtered results, decisions, diagnostics, summary. + +The adapter seams are also correct: + +- `RelationshipPolicyAdapter` for OpenFGA/SpiceDB/Zanzibar-style checks. +- `RulePolicyAdapter` for OPA/Rego, Cedar, or other ABAC engines. + +This follow-up adds the missing interface layer: + +```text +OIDC token or SAML assertion -> verified EnterpriseIdentity -> PolicySubject +``` + +Concrete adapters must validate issuer, audience, expiry, signature, and +assurance metadata before any authorization decision is made. The core package +now exposes protocol/data boundaries for this without taking a dependency on +Keycloak, Entra, LDAP, SCIM, OpenFGA, OPA, or Cedar client libraries. + +## Canonical Subject Mapping + +Recommended normalized shape: + +```yaml +subject: + id: "oidc:#" + display_name: "Ada Lovelace" + principal_type: human | service + issuer: "https://sso.example.org/realms/netkingdom" + audience: ["markitect-tool"] + authorized_party: "markitect-cli" + roles: [viewer, operator] + scopes: [openid, profile, hub:read] + groups: + - /markitect/readers + - /platform/architecture + allowed_labels: [public, internal] + trust_zones: [public, internal] + assurance: + mfa: true + acr: "..." + amr: ["pwd", "otp"] + directory: + source: keycloak | entra | ldap | scim + refreshed_at: "2026-05-04T10:00:00Z" +``` + +The mapping file should translate enterprise groups and app roles to Markitect +labels and actions: + +```yaml +id: markitect-enterprise-policy-map +issuer: https://sso.example.org/realms/netkingdom +audiences: [markitect-tool] +groups: + /markitect/readers: + allowed_labels: [public, internal] + actions: [query, search, read] + /markitect/stewards: + allowed_labels: [public, internal, restricted] + actions: [query, search, read, package, export] +roles: + admin: + allowed_labels: [public, internal, restricted] + actions: [query, search, read, package, export, policy-admin] +scopes: + markitect:read: + actions: [query, search, read] +trust_zones: + internal: + required_groups: [/markitect/readers] +``` + +## Data/Object Mapping + +Markdown remains the source-friendly object labeling layer: + +```yaml +--- +policy: + labels: [internal] + trust_zone: internal + owner: team:platform-architecture +--- +``` + +For enterprise environments, object metadata should eventually include: + +- content labels/classification +- repository/path +- owning team or project +- business domain +- tenancy/org +- retention or export constraints +- provenance of label assignment + +Path rules remain useful as a safety net, but document labels and backend +metadata should become authoritative for cached/context-package retrieval. + +## Enforcement Points + +Markitect should enforce policy at every point where knowledge leaves a +boundary: + +- `mkt cache query` +- `mkt search` +- context package creation and activation +- workflow outputs +- assisted/LLM prompt assembly +- export/render steps +- backend APIs or future MCP resources + +The highest-risk point is context assembly for agents. Before WP-0008 turns +context caches into agent memory, policy must be able to answer: + +- who is activating the context package? +- which token or service account is acting? +- which labels/trust zones are included? +- was anything denied or redacted? +- can this package be reactivated later under the same policy assumptions? + +## Interface Confirmation + +The WP-0009 infrastructure is a good foundation. It already has: + +- policy gateway protocol +- local label gateway +- explainable decisions +- filter-before-return behavior +- query/search integration +- relationship and rule adapter boundaries + +This follow-up adds these provider-neutral interfaces in +`markitect_tool.policy.adapters`: + +1. `IdentityClaimsAdapter` + - validates OIDC/JWT/SAML assertions + - normalizes NetKingdom/key-cape-compatible claims into + `EnterpriseIdentity` + +2. `DirectoryGroupResolver` + - resolves group overage or stale claims + - supports SCIM/Graph/LDAP/Keycloak admin APIs behind adapters + - records freshness and source provenance + +3. `EnterprisePolicyMapper` + - maps canonical groups, roles, scopes, and tenants to Markitect labels, + trust zones, actions, and object constraints + +4. `DecisionLogStore` + - persists policy decisions for query/context/workflow runs + - records token hash, subject id, action, object id, policy version, result, + reason, and redaction status + +5. workflow/runtime policy injection + - `subject_from_token` + - `policy_map` + - `required_assurance` + - `emergency_justification` + +Concrete implementations remain future work. This is deliberate: key-cape and +NetKingdom should own identity issuance and profile compliance, while +Markitect owns normalization, policy envelopes, diagnostics, and enforcement at +knowledge boundaries. + +## Recommended Direction + +Do not make AD/LDAP/Entra groups a Markitect core dependency. + +Instead: + +1. Accept NetKingdom/key-cape-compatible OIDC tokens. +2. Normalize claims into `PolicySubject`. +3. Map enterprise groups/roles/scopes to Markitect labels/trust zones/actions. +4. Use the existing `AccessPolicyGateway` as the enforcement point. +5. Let OpenFGA/SpiceDB/OPA/Cedar attach through adapter protocols where a + deployment needs stronger central policy. +6. Persist decisions before using this for production agent memory or exports. + +## Sources + +- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html +- OAuth 2.0 Token Exchange, RFC 8693: https://www.rfc-editor.org/rfc/rfc8693 +- SCIM Protocol, RFC 7644: https://datatracker.ietf.org/doc/rfc7644 +- NIST SP 800-162 ABAC guide: https://csrc.nist.gov/pubs/sp/800/162/upd2/final +- NIST glossary, Policy Enforcement Point: + https://csrc.nist.gov/glossary/term/policy_enforcement_point +- NIST glossary, Policy Decision Point: + https://csrc.nist.gov/glossary/term/policy_decision_point +- NIST glossary, Policy Information Point: + https://csrc.nist.gov/glossary/term/policy_information_point +- Keycloak Server Administration Guide: https://www.keycloak.org/docs/latest/server_admin/ +- Microsoft Entra group claims: https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles +- OpenFGA concepts: https://openfga.dev/docs/concepts +- Open Policy Agent policy language: https://www.openpolicyagent.org/docs/policy-language +- Cedar policy language: https://docs.cedarpolicy.com/ +- Local canon: `/home/worsch/the-custodian/canon/standards/iam-profile_v0.1.md` diff --git a/docs/workplan-planning-map.md b/docs/workplan-planning-map.md index e58a7d3..39ba3e9 100644 --- a/docs/workplan-planning-map.md +++ b/docs/workplan-planning-map.md @@ -38,6 +38,7 @@ and descriptions mirror the operational view. | `MKTT-WP-0005` | complete | done | `MKTT-WP-0003`, `MKTT-WP-0004` | Runtime context, form state, dynamic rules, workflow integration, and provider-neutral assessment boundary are complete. | | `MKTT-WP-0011` | complete | done | `MKTT-WP-0003`; task-level triggers: `MKTT-WP-0010-T001`, `MKTT-WP-0010-T005` | Markdown dataflow workflow layer is complete: workflow standard, source collectors, binding model, deterministic steps, assisted boundary, safe outputs, CLI, docs, and examples. | | `MKTT-WP-0009` | complete | done | `MKTT-WP-0006` | Access-controlled knowledge gateway is complete: local labels, trust zones, path rules, policy-aware cache query/search, decisions, diagnostics, and external adapter boundaries. | +| `MKTT-WP-0014` | P2 | todo | `MKTT-WP-0009` | Enterprise IAM access-control integration: NetKingdom/key-cape-compatible identity claims, directory group resolution, policy maps, durable decision logs, and external PDP examples. | | `MKTT-WP-0012` | P3 | todo | `MKTT-WP-0004`, `MKTT-WP-0010`, `MKTT-WP-0011` | Future Quarkdown-inspired document function layer: reusable Markdown-native function calls over processors, references, contracts, workflows, and later assisted steps. | | `MKTT-WP-0008` | P3 | todo | `MKTT-WP-0006`, `MKTT-WP-0007`, `MKTT-WP-0009` | Agent working-memory cache after backend and policy floor are available. | @@ -74,6 +75,12 @@ operations deserve author-facing function syntax. It should remain optional and capability-gated, especially before assisted, external, file, or network functions are allowed. +`MKTT-WP-0014` captures enterprise IAM integration for the access-control +gateway. It should follow `MKTT-WP-0009` and can run before or alongside +security-sensitive context memory work. It does not block local `MKTT-WP-0008` +research, but it should gate production deployment of reactivatable agent +context packages in enterprise environments. + ## State Hub Mirror Native State Hub dependency edges should mirror the whole-workstream @@ -100,6 +107,7 @@ dependencies: - `MKTT-WP-0012 -> MKTT-WP-0004` - `MKTT-WP-0012 -> MKTT-WP-0010` - `MKTT-WP-0012 -> MKTT-WP-0011` +- `MKTT-WP-0014 -> MKTT-WP-0009` - `MKTT-WP-0008 -> MKTT-WP-0006` - `MKTT-WP-0008 -> MKTT-WP-0007` - `MKTT-WP-0008 -> MKTT-WP-0009` diff --git a/src/markitect_tool/extension/builtins.py b/src/markitect_tool/extension/builtins.py index cfde167..6ac537d 100644 --- a/src/markitect_tool/extension/builtins.py +++ b/src/markitect_tool/extension/builtins.py @@ -219,8 +219,12 @@ def _local_label_policy_descriptor() -> ExtensionDescriptor: examples=["examples/policy/local-label-policy.yaml"], metadata={ "external_adapters": [ + "IdentityClaimsAdapter", + "DirectoryGroupResolver", + "EnterprisePolicyMapper", "RelationshipPolicyAdapter", "RulePolicyAdapter", + "DecisionLogStore", ] }, ) diff --git a/src/markitect_tool/policy/__init__.py b/src/markitect_tool/policy/__init__.py index 139f27f..86c08ac 100644 --- a/src/markitect_tool/policy/__init__.py +++ b/src/markitect_tool/policy/__init__.py @@ -1,6 +1,14 @@ """Access policy gateways and adapter protocols.""" from markitect_tool.policy.adapters import ( + DecisionLogStore, + DirectoryGroupResolution, + DirectoryGroupResolutionRequest, + DirectoryGroupResolver, + EnterpriseIdentity, + EnterprisePolicyMapRequest, + EnterprisePolicyMapper, + IdentityClaimsAdapter, RelationshipPolicyAdapter, RelationshipPolicyRequest, RulePolicyAdapter, @@ -23,6 +31,14 @@ __all__ = [ "LocalLabelPolicy", "LocalLabelPolicyGateway", "LocalPathPolicyRule", + "DecisionLogStore", + "DirectoryGroupResolution", + "DirectoryGroupResolutionRequest", + "DirectoryGroupResolver", + "EnterpriseIdentity", + "EnterprisePolicyMapRequest", + "EnterprisePolicyMapper", + "IdentityClaimsAdapter", "PolicyDecision", "PolicyFilterResult", "PolicyObject", diff --git a/src/markitect_tool/policy/adapters.py b/src/markitect_tool/policy/adapters.py index 8f9a3a3..16fe7e7 100644 --- a/src/markitect_tool/policy/adapters.py +++ b/src/markitect_tool/policy/adapters.py @@ -1,11 +1,168 @@ -"""Protocol boundaries for external authorization engines.""" +"""Protocol boundaries for external identity and authorization engines.""" from __future__ import annotations from dataclasses import asdict, dataclass, field from typing import Any, Protocol -from markitect_tool.policy.models import PolicyDecision +from markitect_tool.policy.models import PolicyDecision, PolicySubject + + +@dataclass(frozen=True) +class EnterpriseIdentity: + """Verified enterprise identity claims normalized for policy mapping. + + Concrete adapters are responsible for validating tokens or assertions before + returning this shape. Core Markitect only consumes the normalized identity. + """ + + issuer: str + subject: str + identity_scheme: str = "oidc" + principal_type: str = "human" + audience: list[str] = field(default_factory=list) + authorized_party: str | None = None + preferred_username: str | None = None + roles: list[str] = field(default_factory=list) + scopes: list[str] = field(default_factory=list) + groups: list[str] = field(default_factory=list) + assurance: dict[str, Any] = field(default_factory=dict) + directory: dict[str, Any] = field(default_factory=dict) + claims: dict[str, Any] = field(default_factory=dict) + provenance: dict[str, Any] = field(default_factory=dict) + + @property + def canonical_id(self) -> str: + return f"{self.identity_scheme}:{self.issuer}#{self.subject}" + + def to_policy_subject( + self, + *, + allowed_labels: list[str] | None = None, + trust_zones: list[str] | None = None, + allowed_actions: list[str] | None = None, + attributes: dict[str, Any] | None = None, + ) -> PolicySubject: + """Convert verified claims into the subject shape used by gateways.""" + + return PolicySubject( + id=self.canonical_id, + allowed_labels=list(allowed_labels or []), + trust_zones=list(trust_zones or []), + roles=list(self.roles), + allowed_actions=list(allowed_actions or []), + attributes={ + "issuer": self.issuer, + "subject": self.subject, + "identity_scheme": self.identity_scheme, + "principal_type": self.principal_type, + "audience": self.audience, + "authorized_party": self.authorized_party, + "preferred_username": self.preferred_username, + "scopes": self.scopes, + "groups": self.groups, + "assurance": self.assurance, + "directory": self.directory, + "identity_provenance": self.provenance, + } + | dict(attributes or {}), + ) + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["canonical_id"] = self.canonical_id + return _drop_empty(data) + + +class IdentityClaimsAdapter(Protocol): + """Adapter boundary for OIDC/JWT/SAML verification and normalization.""" + + def verify( + self, + token_or_assertion: str | dict[str, Any], + *, + context: dict[str, Any] | None = None, + ) -> EnterpriseIdentity | dict[str, Any]: + """Validate identity material and return normalized enterprise claims.""" + + +@dataclass(frozen=True) +class DirectoryGroupResolutionRequest: + """Request for resolving group claims that are stale, partial, or overlarge.""" + + subject_id: str + issuer: str + groups: list[str] = field(default_factory=list) + claims: dict[str, Any] = field(default_factory=dict) + context: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return _drop_empty(asdict(self)) + + +@dataclass(frozen=True) +class DirectoryGroupResolution: + """Directory group resolution result with freshness and source provenance.""" + + groups: list[str] = field(default_factory=list) + source: str | None = None + refreshed_at: str | None = None + overage: bool = False + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return _drop_empty(asdict(self)) + + +class DirectoryGroupResolver(Protocol): + """Adapter boundary for SCIM/LDAP/Graph/Keycloak group resolution.""" + + def resolve( + self, + request: DirectoryGroupResolutionRequest, + ) -> DirectoryGroupResolution | dict[str, Any]: + """Return groups plus freshness/provenance metadata.""" + + +@dataclass(frozen=True) +class EnterprisePolicyMapRequest: + """Request to map enterprise claims onto Markitect policy vocabulary.""" + + identity: EnterpriseIdentity | dict[str, Any] + policy_map: dict[str, Any] = field(default_factory=dict) + groups: list[str] = field(default_factory=list) + context: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + if isinstance(self.identity, EnterpriseIdentity): + data["identity"] = self.identity.to_dict() + return _drop_empty(data) + + +class EnterprisePolicyMapper(Protocol): + """Adapter boundary for mapping IAM roles/groups/scopes to policy subjects.""" + + def map_subject( + self, + request: EnterprisePolicyMapRequest, + ) -> PolicySubject | dict[str, Any]: + """Return a gateway-ready subject with labels, zones, and actions.""" + + +class DecisionLogStore(Protocol): + """Persistent audit boundary for policy decisions.""" + + def record( + self, + decision: PolicyDecision, + *, + context: dict[str, Any] | None = None, + ) -> str: + """Persist a decision and return its durable audit id.""" + + def get(self, decision_id: str) -> dict[str, Any] | None: + """Return one recorded decision when available.""" @dataclass(frozen=True) diff --git a/tests/test_builtin_extension_catalog.py b/tests/test_builtin_extension_catalog.py index 1c38405..568b900 100644 --- a/tests/test_builtin_extension_catalog.py +++ b/tests/test_builtin_extension_catalog.py @@ -96,4 +96,5 @@ def test_builtin_policy_descriptor_exposes_cli_and_adapter_boundary(): "policy_filter", } assert "mkt policy check" in descriptor.cli["commands"] + assert "IdentityClaimsAdapter" in descriptor.metadata["external_adapters"] assert "RelationshipPolicyAdapter" in descriptor.metadata["external_adapters"] diff --git a/tests/test_policy_gateway.py b/tests/test_policy_gateway.py index d16010b..c25ff98 100644 --- a/tests/test_policy_gateway.py +++ b/tests/test_policy_gateway.py @@ -4,7 +4,14 @@ from pathlib import Path from click.testing import CliRunner from markitect_tool.cli import main -from markitect_tool.policy import LocalLabelPolicy, LocalLabelPolicyGateway +from markitect_tool.policy import ( + DirectoryGroupResolution, + DirectoryGroupResolutionRequest, + EnterpriseIdentity, + EnterprisePolicyMapRequest, + LocalLabelPolicy, + LocalLabelPolicyGateway, +) POLICY_TEXT = """id: example-policy @@ -187,6 +194,59 @@ def test_mkt_cache_query_filters_indexed_documents_by_policy(tmp_path: Path): assert data["policy"]["denied"] == 1 +def test_enterprise_identity_maps_to_policy_subject(): + identity = EnterpriseIdentity( + issuer="https://sso.example.test/realms/netkingdom", + subject="user-123", + preferred_username="ada", + roles=["viewer"], + scopes=["markitect:read"], + groups=["/markitect/readers"], + assurance={"mfa": True}, + directory={"source": "keycloak"}, + ) + + subject = identity.to_policy_subject( + allowed_labels=["public", "internal"], + trust_zones=["public", "internal"], + allowed_actions=["query", "search"], + ) + + assert subject.id == "oidc:https://sso.example.test/realms/netkingdom#user-123" + assert subject.roles == ["viewer"] + assert subject.allowed_labels == ["public", "internal"] + assert subject.allowed_actions == ["query", "search"] + assert subject.attributes["issuer"] == "https://sso.example.test/realms/netkingdom" + assert subject.attributes["groups"] == ["/markitect/readers"] + assert subject.attributes["assurance"]["mfa"] is True + + +def test_enterprise_policy_adapter_requests_serialize_cleanly(): + group_request = DirectoryGroupResolutionRequest( + subject_id="oidc:https://sso.example.test/realms/netkingdom#user-123", + issuer="https://sso.example.test/realms/netkingdom", + claims={"hasgroups": True}, + ) + group_result = DirectoryGroupResolution( + groups=["/markitect/readers"], + source="keycloak", + refreshed_at="2026-05-04T10:00:00Z", + overage=True, + ) + map_request = EnterprisePolicyMapRequest( + identity=EnterpriseIdentity( + issuer="https://sso.example.test/realms/netkingdom", + subject="user-123", + ), + policy_map={"groups": {"/markitect/readers": {"allowed_labels": ["internal"]}}}, + groups=group_result.groups, + ) + + assert group_request.to_dict()["claims"] == {"hasgroups": True} + assert group_result.to_dict()["overage"] is True + assert map_request.to_dict()["identity"]["canonical_id"].endswith("#user-123") + + def _policy_mapping() -> dict: return { "id": "example-policy", diff --git a/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md b/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md new file mode 100644 index 0000000..f05a400 --- /dev/null +++ b/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md @@ -0,0 +1,238 @@ +--- +id: MKTT-WP-0014 +type: workplan +title: "Enterprise IAM Access-Control Integration" +domain: markitect +status: todo +owner: markitect-tool +topic_slug: markitect +planning_priority: P2 +planning_order: 82 +depends_on_workplans: + - MKTT-WP-0009 +related_workplans: + - MKTT-WP-0006 + - MKTT-WP-0007 + - MKTT-WP-0008 + - MKTT-WP-0011 + - MKTT-WP-0013 +created: "2026-05-04" +updated: "2026-05-04" +state_hub_workstream_id: "86c22ccc-5f5a-4650-8495-76fe6c08e411" +--- + +# MKTT-WP-0014: Enterprise IAM Access-Control Integration + +## Purpose + +Turn the local access-control gateway into an enterprise-ready integration +surface without making Markitect an identity provider or hard-coding one +directory vendor. + +Markitect should act as the policy enforcement point for Markdown knowledge +results. NetKingdom/key-cape-compatible SSO should supply identity claims. +External policy engines and enterprise directories should attach through +provider-neutral adapters. + +## Background + +`MKTT-WP-0009` implemented local labels, trust zones, path rules, query/search +filtering, explainable decisions, and relationship/rule policy adapter +boundaries. The enterprise follow-up research showed a clear canonical shape: + +- OIDC/SAML for authentication and signed identity assertions. +- SCIM/LDAP/Graph/Keycloak admin APIs for directory and group information. +- PEP/PDP/PIP/PAP separation for authorization architecture. +- RBAC/ABAC/ReBAC policy models through mappable policy decision points. +- NetKingdom IAM profile as the local identity contract, with key-cape as the + preferred lightweight/bootstrap path. + +Initial provider-neutral interfaces now exist in +`markitect_tool.policy.adapters`: + +- `EnterpriseIdentity` +- `IdentityClaimsAdapter` +- `DirectoryGroupResolver` +- `EnterprisePolicyMapper` +- `DecisionLogStore` + +Documentation: `docs/enterprise-access-control-integration.md`. + +## Decision + +Implement concrete enterprise integration as an optional extension track. Core +Markitect should keep accepting normalized `PolicySubject` and `PolicyObject` +models, while enterprise adapters handle token verification, group freshness, +claim mapping, durable decision logs, and external PDP calls. + +Do not map raw AD/LDAP/Entra group names directly to Markitect privileges. +Always map: + +```text +directory groups -> canonical roles/scopes/trust labels -> PolicySubject +``` + +## P14.1 - Define enterprise policy map schema + +```task +id: MKTT-WP-0014-T001 +status: todo +priority: high +state_hub_task_id: "1894c50f-95c3-4e1a-bd4f-388f7624ebd7" +``` + +Define the mapping file that translates enterprise groups, roles, scopes, +tenants, assurance levels, and emergency rules into Markitect labels, trust +zones, allowed actions, and object constraints. + +Output: schema, examples, diagnostics, and tests. + +## P14.2 - Implement NetKingdom/key-cape identity claims adapter + +```task +id: MKTT-WP-0014-T002 +status: todo +priority: high +state_hub_task_id: "8a177375-09b3-4898-a053-7601f82fcb29" +``` + +Implement an optional `IdentityClaimsAdapter` that consumes +NetKingdom/key-cape-compatible OIDC discovery and JWTs. + +It must validate: + +- issuer +- audience +- expiry and issued-at +- signature through JWKS +- authorized party/client id where required +- MFA/assurance claims for privileged actions + +Output: adapter, fixtures, negative tests, and clear diagnostics. + +## P14.3 - Implement enterprise subject mapper + +```task +id: MKTT-WP-0014-T003 +status: todo +priority: high +state_hub_task_id: "6861d4bc-1bb8-440d-bb9e-33e20c7feb55" +``` + +Implement `EnterprisePolicyMapper` over the policy map schema. It should map +verified identity claims and resolved groups into gateway-ready +`PolicySubject` objects. + +Output: mapper, examples, and tests for roles, scopes, groups, trust zones, +tenancy, and emergency access. + +## P14.4 - Add directory group resolution boundary + +```task +id: MKTT-WP-0014-T004 +status: todo +priority: medium +state_hub_task_id: "56d6bad6-d706-47b3-b321-1f0e870ecc0d" +``` + +Implement a provider-neutral group-resolution layer for claims that are stale, +partial, or too large for tokens. Start with a fake/test resolver and specify +adapter hooks for SCIM, Microsoft Graph, LDAP, and Keycloak. + +Output: resolver contract, freshness metadata, overage handling, and tests. + +## P14.5 - Persist decision logs + +```task +id: MKTT-WP-0014-T005 +status: todo +priority: high +state_hub_task_id: "f212662c-4ffc-4cac-ace2-a43777f4960c" +``` + +Implement a durable `DecisionLogStore` for policy decisions from query, search, +context packages, workflows, exports, and assisted prompt assembly. + +Decision logs should record subject id, token hash, action, object id, policy +version, decision effect, reason, redaction status, and provenance. + +Output: storage adapter, CLI inspection path, and tests. + +## P14.6 - Add external PDP examples + +```task +id: MKTT-WP-0014-T006 +status: todo +priority: medium +state_hub_task_id: "573a198f-df0b-470a-b11c-9ac839c0845e" +``` + +Provide reference adapters or documented examples for: + +- OpenFGA/SpiceDB-style relationship checks through + `RelationshipPolicyAdapter` +- OPA/Rego or Cedar-style rule checks through `RulePolicyAdapter` + +Output: examples, adapter stubs, and policy request/decision fixtures. + +## P14.7 - Integrate policy identity into workflows and context packages + +```task +id: MKTT-WP-0014-T007 +status: todo +priority: high +state_hub_task_id: "c4650304-0e2b-49c5-8569-e69907c08ccc" +``` + +Make workflow and future context-package execution accept explicit enterprise +identity and policy mapping configuration. + +Required concepts: + +- `subject_from_token` +- `policy_map` +- `required_assurance` +- `emergency_justification` +- decision-log sink + +Output: workflow/context integration design, examples, and tests. + +## P14.8 - Validate against NetKingdom IAM profile + +```task +id: MKTT-WP-0014-T008 +status: todo +priority: medium +state_hub_task_id: "0486e0c2-2cb9-4902-9a09-9ec729e9e79f" +``` + +Build conformance tests against the local IAM profile: + +- required claims +- human Authorization Code + PKCE expectations +- service account claims +- local development issuer rejection in production mode +- emergency access audit requirements + +Output: test fixtures and conformance checklist. + +## Exit Criteria + +- A NetKingdom/key-cape-compatible OIDC identity can be validated and mapped to + a `PolicySubject`. +- Enterprise groups, roles, scopes, trust zones, and labels are mapped through + a versioned policy map rather than raw directory names. +- Query, search, workflow, and context-package boundaries can enforce policy + and emit durable decision logs. +- Directory group overage and freshness are represented explicitly. +- OpenFGA/SpiceDB and OPA/Cedar-style PDP integrations can attach without + replacing Markitect's local policy gateway. +- The implementation remains optional and does not add enterprise IAM + dependencies to core Markdown parsing or deterministic processing. + +## Notes + +This workplan should be picked up before using Markitect context caches for +production agent memory in enterprise settings. It does not need to block local +research on `MKTT-WP-0008`, but it should gate production deployment of +reactivatable cross-document context packages.