diff --git a/examples/markitect/check_fixtures.yaml b/examples/markitect/check_fixtures.yaml new file mode 100644 index 0000000..75d9c9c --- /dev/null +++ b/examples/markitect/check_fixtures.yaml @@ -0,0 +1,239 @@ +- id: fixture:markitect-public-document-allow + request: + id: check:markitect-public-document + subject: + id: user:visitor + type: Human + tenant: tenant:alpha + action: read + resource: + id: document:public-note + type: document + system: markitect-tool + tenant: tenant:alpha + attributes: + labels: + - public + trust_zone: public + caring_context: + id: descriptor:public-document-reader + profile: caring-0.4.0-rc2 + subject_type: Human + organization_relation: Customer + canonical_role: Doer + scope: + level: Resource + id: document:public-note + tenant: tenant:alpha + planes: + - Data + capabilities: + - View + exposure_modes: + - Plaintext + conditions: + - Logged + expect: + effect: allow + reason: public_document + metadata: + expected_caring_descriptor: descriptor:public-document-reader + expected_conformance_findings: [] + expected_exposure_modes: + - Plaintext + expected_audit_behavior: sampled_allow +- id: fixture:markitect-internal-document-deny + request: + id: check:markitect-internal-document-deny + subject: + id: user:visitor + type: Human + tenant: tenant:alpha + attributes: + groups: [] + action: read + resource: + id: document:internal-note + type: document + system: markitect-tool + tenant: tenant:alpha + attributes: + labels: + - internal + trust_zone: internal + expect: + effect: deny + reason: no_matching_rule + metadata: + expected_caring_descriptor: null + expected_conformance_findings: [] + expected_exposure_modes: + - None + expected_audit_behavior: always_record +- id: fixture:markitect-internal-document-reader-allow + request: + id: check:markitect-internal-document-reader + subject: + id: user:alice + type: Human + tenant: tenant:alpha + attributes: + groups: + - group:platform-architecture + action: read + resource: + id: document:internal-note + type: document + system: markitect-tool + tenant: tenant:alpha + attributes: + labels: + - internal + trust_zone: internal + caring_context: + id: descriptor:internal-document-reader + profile: caring-0.4.0-rc2 + subject_type: Human + organization_relation: Customer + canonical_role: Doer + scope: + level: Resource + id: document:internal-note + tenant: tenant:alpha + planes: + - Data + capabilities: + - View + exposure_modes: + - Masked + - Plaintext + conditions: + - Logged + restrictions: + - ExportBlocked + expect: + effect: allow + reason: reader_group + metadata: + expected_caring_descriptor: descriptor:internal-document-reader + expected_conformance_findings: [] + expected_exposure_modes: + - Masked + - Plaintext + expected_audit_behavior: sampled_allow +- id: fixture:markitect-restricted-export-steward-mfa + request: + id: check:markitect-restricted-export + subject: + id: user:steward + type: Human + tenant: tenant:alpha + attributes: + roles: + - steward + action: export + resource: + id: export:internal-note-review-bundle + type: export + system: markitect-tool + tenant: tenant:alpha + attributes: + labels: + - export + trust_zone: external + context: + mfa: true + reason: customer-approved export + caring_context: + id: descriptor:restricted-export-steward + profile: caring-0.4.0-rc2 + subject_type: Human + organization_relation: Customer + canonical_role: Maintainer + scope: + level: Record + id: export:internal-note-review-bundle + tenant: tenant:alpha + planes: + - Data + - Audit + capabilities: + - Export + exposure_modes: + - Exportable + - Plaintext + conditions: + - MFARequired + - Logged + expect: + effect: allow + reason: steward_export_mfa + conformance_findings: + - code: MARKITECT-EXPORT-MFA-LOGGED + severity: info + message: Export is allowed only with steward role, MFA, and logging. + metadata: + expected_caring_descriptor: descriptor:restricted-export-steward + expected_exposure_modes: + - Exportable + - Plaintext + expected_audit_behavior: always_record +- id: fixture:markitect-context-package-activation + request: + id: check:markitect-context-package-activation + subject: + id: user:alice + type: Human + tenant: tenant:alpha + action: activate_context + resource: + id: context-package:internal-note-review + type: context_package + system: markitect-tool + tenant: tenant:alpha + attributes: + labels: + - internal + - generated + context: + freshness_seconds: 600 + policy_version: markitect-gateway-v1 + caring_context: + id: descriptor:context-package-activation + profile: caring-0.4.0-rc2 + subject_type: Human + organization_relation: Customer + canonical_role: Verifier + scope: + level: Dataset + id: context-package:internal-note-review + tenant: tenant:alpha + planes: + - Intent + - Policy + capabilities: + - Use + - Execute + exposure_modes: + - Metadata + - Masked + conditions: + - PurposeBound + - Logged + expect: + effect: allow + reason: fresh_context_package + obligations: + - type: record_context_activation + parameters: + freshness_seconds: 600 + conformance_findings: + - code: MARKITECT-CONTEXT-FRESHNESS + severity: info + message: Context package activation includes policy version and freshness metadata. + metadata: + expected_caring_descriptor: descriptor:context-package-activation + expected_exposure_modes: + - Metadata + - Masked + expected_audit_behavior: always_record diff --git a/examples/markitect/check_policy_package.md b/examples/markitect/check_policy_package.md new file mode 100644 index 0000000..f5a3080 --- /dev/null +++ b/examples/markitect/check_policy_package.md @@ -0,0 +1,152 @@ +--- +id: markitect.gateway.check-fixtures +name: Markitect gateway check fixtures +namespace: markitect:gateway +version: v1 +status: draft +package: flexauth.markitect.gateway +actions: + - read + - export + - activate_context +owner: team:platform-architecture +fixtures: + - check_fixtures.yaml +caring: + profile: caring-0.4.0-rc2 + enforce: false + canonical_roles: + - Doer + - Maintainer + - Verifier + organization_relations: + - Customer + scopes: + - level: Resource + id: document:public-note + tenant: tenant:alpha + - level: Resource + id: document:internal-note + tenant: tenant:alpha + - level: Dataset + id: context-package:internal-note-review + tenant: tenant:alpha + planes: + - Intent + - Data + - Audit + capabilities: + - View + - Export + - Use + - Execute + exposure_modes: + - Metadata + - Masked + - Plaintext + - Exportable + conditions: + - MFARequired + - PurposeBound + - Logged + restrictions: + - ExportBlocked +metadata: + source: examples/markitect/check_policy_package.md +--- + +# Markitect Gateway Check Fixtures + +This package captures the first Markitect gateway scenarios as executable Rego +and external fixtures. + +## Rules + +```rego +import future.keywords.if +import future.keywords.in + +default decision := {"effect": "deny", "reason": "no_matching_rule"} + +decision := {"effect": "allow", "reason": "public_document"} if { + input.action == "read" + input.resource.type == "document" + "public" in object.get(input.resource.attributes, "labels", []) +} + +decision := {"effect": "allow", "reason": "reader_group"} if { + input.action == "read" + input.resource.type == "document" + "internal" in object.get(input.resource.attributes, "labels", []) + "group:platform-architecture" in object.get(input.subject.attributes, "groups", []) + "View" in input.caring_context.capabilities +} + +decision := { + "effect": "allow", + "reason": "steward_export_mfa", + "conformance_findings": [{ + "code": "MARKITECT-EXPORT-MFA-LOGGED", + "severity": "info", + "message": "Export is allowed only with steward role, MFA, and logging." + }] +} if { + input.action == "export" + "steward" in object.get(input.subject.attributes, "roles", []) + input.context.mfa == true + "Export" in input.caring_context.capabilities + "Exportable" in input.caring_context.exposure_modes +} + +decision := { + "effect": "allow", + "reason": "fresh_context_package", + "obligations": [{ + "type": "record_context_activation", + "parameters": {"freshness_seconds": input.context.freshness_seconds} + }], + "conformance_findings": [{ + "code": "MARKITECT-CONTEXT-FRESHNESS", + "severity": "info", + "message": "Context package activation includes policy version and freshness metadata." + }] +} if { + input.action == "activate_context" + input.resource.type == "context_package" + input.policy_version != "" + input.context.freshness_seconds <= 900 + "Use" in input.caring_context.capabilities + "Execute" in input.caring_context.capabilities +} +``` + +## Tests + +```rego test +package flexauth.markitect.gateway_test + +import future.keywords.if +import data.flexauth.markitect.gateway + +test_public_document_allows if { + gateway.decision.effect == "allow" with input as { + "action": "read", + "resource": { + "type": "document", + "attributes": {"labels": ["public"]} + } + } +} + +test_export_requires_mfa if { + gateway.decision.effect == "deny" with input as { + "action": "export", + "subject": {"attributes": {"roles": ["steward"]}}, + "context": {"mfa": false}, + "caring_context": { + "capabilities": ["Export"], + "exposure_modes": ["Exportable"] + } + } +} +``` diff --git a/internal/markitect/check_fixtures_test.go b/internal/markitect/check_fixtures_test.go new file mode 100644 index 0000000..61d7a02 --- /dev/null +++ b/internal/markitect/check_fixtures_test.go @@ -0,0 +1,57 @@ +package markitect_test + +import ( + "context" + "path/filepath" + "testing" + + "github.com/netkingdom/flex-auth/internal/policy" + "github.com/netkingdom/flex-auth/pkg/api" +) + +func TestMarkitectCheckFixturePackageValidates(t *testing.T) { + pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "markitect", "check_policy_package.md")) + if err != nil { + t.Fatalf("LoadAndValidateFile: %v", err) + } + if !pkg.Valid { + t.Fatalf("pkg.Valid = false: %+v", pkg.Validation) + } + if len(pkg.Fixtures) != 5 { + t.Fatalf("Fixtures len = %d; want 5", len(pkg.Fixtures)) + } + + wantEffects := map[string]api.DecisionEffect{ + "fixture:markitect-public-document-allow": api.DecisionEffectAllow, + "fixture:markitect-internal-document-deny": api.DecisionEffectDeny, + "fixture:markitect-internal-document-reader-allow": api.DecisionEffectAllow, + "fixture:markitect-restricted-export-steward-mfa": api.DecisionEffectAllow, + "fixture:markitect-context-package-activation": api.DecisionEffectAllow, + } + for _, fixture := range pkg.Fixtures { + if fixture.Expect.Effect != wantEffects[fixture.ID] { + t.Fatalf("%s effect = %q; want %q", fixture.ID, fixture.Expect.Effect, wantEffects[fixture.ID]) + } + assertFixtureMetadata(t, fixture) + } + + for _, result := range pkg.Validation.Fixtures { + if !result.Passed { + t.Fatalf("fixture %s failed: %s actual=%+v", result.ID, result.Error, result.Actual) + } + } +} + +func assertFixtureMetadata(t *testing.T, fixture api.PolicyFixture) { + t.Helper() + + if _, ok := fixture.Metadata["expected_caring_descriptor"]; !ok { + t.Fatalf("%s missing expected_caring_descriptor metadata", fixture.ID) + } + if _, ok := fixture.Metadata["expected_exposure_modes"]; !ok { + t.Fatalf("%s missing expected_exposure_modes metadata", fixture.ID) + } + if _, ok := fixture.Metadata["expected_audit_behavior"]; !ok { + t.Fatalf("%s missing expected_audit_behavior metadata", fixture.ID) + } +} diff --git a/internal/policy/package.go b/internal/policy/package.go index 4c9a3cd..bf28492 100644 --- a/internal/policy/package.go +++ b/internal/policy/package.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "reflect" "sort" "strings" @@ -413,15 +412,21 @@ func expectationMatches(expected, actual api.DecisionExpectation) bool { if expected.Reason != "" && expected.Reason != actual.Reason { return false } - if len(expected.Obligations) > 0 && !reflect.DeepEqual(expected.Obligations, actual.Obligations) { + if len(expected.Obligations) > 0 && !jsonEqual(expected.Obligations, actual.Obligations) { return false } - if len(expected.ConformanceFindings) > 0 && !reflect.DeepEqual(expected.ConformanceFindings, actual.ConformanceFindings) { + if len(expected.ConformanceFindings) > 0 && !jsonEqual(expected.ConformanceFindings, actual.ConformanceFindings) { return false } return true } +func jsonEqual(left, right any) bool { + leftData, leftErr := json.Marshal(left) + rightData, rightErr := json.Marshal(right) + return leftErr == nil && rightErr == nil && string(leftData) == string(rightData) +} + func toRegoInput(value any) (map[string]any, error) { data, err := json.Marshal(value) if err != nil { diff --git a/workplans/FLEX-WP-0003-markitect-consumer-integration.md b/workplans/FLEX-WP-0003-markitect-consumer-integration.md index b1a8036..6626fc1 100644 --- a/workplans/FLEX-WP-0003-markitect-consumer-integration.md +++ b/workplans/FLEX-WP-0003-markitect-consumer-integration.md @@ -103,7 +103,7 @@ metadata-only exposure explicit. ```task id: FLEX-WP-0003-T004 -status: todo +status: done priority: high state_hub_task_id: "1d5de3b2-c581-4ca3-9107-93211eb02c6b" ```