From 7fdf6d63d5cf0bbc61ab045586e76674f5c7b197 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 17 May 2026 04:59:18 +0200 Subject: [PATCH] Implement canonical schema foundation --- examples/README.md | 2 + examples/caring/README.md | 8 + examples/caring/access_descriptor.yaml | 26 ++ examples/caring/audit_event.json | 22 ++ examples/caring/check_request.yaml | 41 ++ examples/caring/decision_envelope.json | 69 ++++ examples/caring/policy_fixture.yaml | 45 +++ examples/caring/policy_package.yaml | 29 ++ examples/caring/relationship_fact.yaml | 32 ++ examples/caring/subject_manifest.yaml | 22 ++ internal/policy/package_test.go | 50 +++ internal/registry/manifest_test.go | 43 +++ pkg/api/canonical.go | 249 ++++++++++++ pkg/api/canonical_test.go | 130 +++++++ pkg/api/caring.go | 335 ++++++++++++++++ pkg/api/resource_manifest.go | 28 +- schemas/README.md | 6 +- schemas/audit_event.schema.json | 20 + schemas/caring_access_descriptor.schema.json | 358 ++++++++++++++++++ schemas/caring_profile.schema.json | 16 + schemas/check_request.schema.json | 42 ++ schemas/decision_envelope.schema.json | 74 ++++ schemas/policy_fixture.schema.json | 33 ++ schemas/policy_package.schema.json | 68 ++++ schemas/protected_system_manifest.schema.json | 69 ++++ schemas/relationship_fact.schema.json | 24 ++ schemas/resource_manifest.schema.json | 9 + schemas/subject_manifest.schema.json | 68 ++++ ...-WP-0002-standalone-policy-as-code-core.md | 2 +- 29 files changed, 1905 insertions(+), 15 deletions(-) create mode 100644 examples/caring/README.md create mode 100644 examples/caring/access_descriptor.yaml create mode 100644 examples/caring/audit_event.json create mode 100644 examples/caring/check_request.yaml create mode 100644 examples/caring/decision_envelope.json create mode 100644 examples/caring/policy_fixture.yaml create mode 100644 examples/caring/policy_package.yaml create mode 100644 examples/caring/relationship_fact.yaml create mode 100644 examples/caring/subject_manifest.yaml create mode 100644 internal/policy/package_test.go create mode 100644 internal/registry/manifest_test.go create mode 100644 pkg/api/canonical.go create mode 100644 pkg/api/canonical_test.go create mode 100644 pkg/api/caring.go create mode 100644 schemas/audit_event.schema.json create mode 100644 schemas/caring_access_descriptor.schema.json create mode 100644 schemas/caring_profile.schema.json create mode 100644 schemas/check_request.schema.json create mode 100644 schemas/decision_envelope.schema.json create mode 100644 schemas/policy_fixture.schema.json create mode 100644 schemas/policy_package.schema.json create mode 100644 schemas/protected_system_manifest.schema.json create mode 100644 schemas/relationship_fact.schema.json create mode 100644 schemas/subject_manifest.schema.json diff --git a/examples/README.md b/examples/README.md index dfaa16b..505544a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,6 +9,8 @@ FLEX-WP-0005): examples/ claims/ # key-cape lightweight-mode and Keycloak heavy-mode # claim envelopes (P5.5) + caring/ # executable CARING descriptor, request, + # decision, registry, and audit fixtures (P2.1) markitect/ # FlexAuthResourceManifest fixtures, decision # fixtures, and Rego-in-Markdown policy packages topaz/ # docker-compose + sample directory and policy diff --git a/examples/caring/README.md b/examples/caring/README.md new file mode 100644 index 0000000..86d8bf6 --- /dev/null +++ b/examples/caring/README.md @@ -0,0 +1,8 @@ +# CARING examples + +Small fixtures for the executable CARING 0.4.0-RC2 profile used by +`FLEX-WP-0002 P2.1`. + +These are intentionally compact. They are not policy-engine fixtures yet; +they prove that the canonical descriptor, request, decision, registry, and +audit shapes can round-trip through `pkg/api`. diff --git a/examples/caring/access_descriptor.yaml b/examples/caring/access_descriptor.yaml new file mode 100644 index 0000000..683d124 --- /dev/null +++ b/examples/caring/access_descriptor.yaml @@ -0,0 +1,26 @@ +id: descriptor:tenant-alpha-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 + resource: document:internal-note +planes: + - Data +capabilities: + - View +exposure_modes: + - Masked + - Plaintext +conditions: + - PurposeBound + - Logged +lifecycle_state: Operate +restrictions: + - ExportBlocked +access_path: direct +metadata: + source: examples/caring diff --git a/examples/caring/audit_event.json b/examples/caring/audit_event.json new file mode 100644 index 0000000..625565f --- /dev/null +++ b/examples/caring/audit_event.json @@ -0,0 +1,22 @@ +{ + "id": "audit:decision:tenant-alpha-internal-note", + "type": "decision", + "decision_id": "decision:tenant-alpha-internal-note", + "subject": { + "id": "user:alice", + "type": "Human", + "tenant": "tenant:alpha" + }, + "resource": { + "id": "document:internal-note", + "type": "document", + "system": "markitect-tool", + "tenant": "tenant:alpha" + }, + "action": "read", + "effect": "allow", + "timestamp": "2026-05-17T00:00:00Z", + "metadata": { + "profile": "caring-0.4.0-rc2" + } +} diff --git a/examples/caring/check_request.yaml b/examples/caring/check_request.yaml new file mode 100644 index 0000000..fbd34e8 --- /dev/null +++ b/examples/caring/check_request.yaml @@ -0,0 +1,41 @@ +id: check:tenant-alpha-internal-note +subject: + id: user:alice + type: Human + tenant: tenant:alpha +action: read +resource: + id: document:internal-note + type: document + system: markitect-tool + tenant: tenant:alpha +context: + purpose: knowledge-base-read + assurance: + mfa: true +caring_context: + id: descriptor:tenant-alpha-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 + resource: document:internal-note + planes: + - Data + capabilities: + - View + exposure_modes: + - Masked + - Plaintext + conditions: + - PurposeBound + - Logged + lifecycle_state: Operate + restrictions: + - ExportBlocked + access_path: direct +policy_version: markitect.documents.v1 diff --git a/examples/caring/decision_envelope.json b/examples/caring/decision_envelope.json new file mode 100644 index 0000000..e1586fd --- /dev/null +++ b/examples/caring/decision_envelope.json @@ -0,0 +1,69 @@ +{ + "id": "decision:tenant-alpha-internal-note", + "request_id": "check:tenant-alpha-internal-note", + "effect": "allow", + "reason": "reader_relation", + "matched_policy_version": "markitect.documents.v1", + "matched_rule": "allow_document_read", + "resource": { + "id": "document:internal-note", + "type": "document", + "system": "markitect-tool", + "tenant": "tenant:alpha" + }, + "subject": { + "id": "user:alice", + "type": "Human", + "tenant": "tenant:alpha" + }, + "obligations": [ + { + "type": "log_access", + "parameters": { + "level": "standard" + } + } + ], + "diagnostics": { + "policy_package": "examples/caring" + }, + "provenance": { + "evaluator": "flex-auth", + "mode": "standalone", + "policy_package": "markitect.documents", + "policy_version": "v1", + "decision_time": "2026-05-17T00:00:00Z" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "descriptor": { + "id": "descriptor:tenant-alpha-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", + "resource": "document:internal-note" + }, + "planes": ["Data"], + "capabilities": ["View"], + "exposure_modes": ["Masked", "Plaintext"], + "conditions": ["PurposeBound", "Logged"], + "lifecycle_state": "Operate", + "restrictions": ["ExportBlocked"], + "access_path": "direct" + }, + "restrictions_evaluated": ["ExportBlocked"], + "exposure_modes": ["Masked", "Plaintext"], + "conformance_findings": [ + { + "code": "CARING-EXPORT-SEPARATION", + "severity": "info", + "message": "View is allowed, but Exportable exposure remains separately blocked." + } + ] + } +} diff --git a/examples/caring/policy_fixture.yaml b/examples/caring/policy_fixture.yaml new file mode 100644 index 0000000..04247ab --- /dev/null +++ b/examples/caring/policy_fixture.yaml @@ -0,0 +1,45 @@ +id: fixture:markitect-internal-read-allow +request: + id: check:tenant-alpha-internal-note + subject: + id: user:alice + type: Human + tenant: tenant:alpha + action: read + resource: + id: document:internal-note + type: document + system: markitect-tool + tenant: tenant:alpha + caring_context: + id: descriptor:tenant-alpha-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 + resource: document:internal-note + planes: + - Data + capabilities: + - View + exposure_modes: + - Masked + - Plaintext + conditions: + - PurposeBound + - Logged + restrictions: + - ExportBlocked +expect: + effect: allow + reason: reader_relation + conformance_findings: + - code: CARING-EXPORT-SEPARATION + severity: info + message: View is allowed, but Exportable exposure remains separately blocked. +metadata: + source: examples/caring diff --git a/examples/caring/policy_package.yaml b/examples/caring/policy_package.yaml new file mode 100644 index 0000000..40c1965 --- /dev/null +++ b/examples/caring/policy_package.yaml @@ -0,0 +1,29 @@ +id: markitect.documents.internal-read +name: Markitect internal document read +version: v1 +status: draft +package: flexauth.markitect.documents +caring: + profile: caring-0.4.0-rc2 + canonical_roles: + - Doer + organization_relations: + - Customer + scopes: + - level: Resource + id: document:internal-note + tenant: tenant:alpha + planes: + - Data + capabilities: + - View + exposure_modes: + - Masked + - Plaintext + conditions: + - PurposeBound + - Logged + restrictions: + - ExportBlocked +metadata: + source: examples/caring diff --git a/examples/caring/relationship_fact.yaml b/examples/caring/relationship_fact.yaml new file mode 100644 index 0000000..04cf239 --- /dev/null +++ b/examples/caring/relationship_fact.yaml @@ -0,0 +1,32 @@ +id: rel:alice-reader-internal-note +system: markitect-tool +subject: group:platform-architecture +relation: reader +object: document:internal-note +tenant: tenant:alpha +conditions: + - Logged +caring: + id: descriptor:tenant-alpha-document-reader + profile: caring-0.4.0-rc2 + subject_type: Group + organization_relation: Customer + canonical_role: Doer + scope: + level: Resource + id: document:internal-note + tenant: tenant:alpha + resource: document:internal-note + planes: + - Data + capabilities: + - View + exposure_modes: + - Masked + - Plaintext + conditions: + - Logged + restrictions: + - ExportBlocked +provenance: + source: examples/caring diff --git a/examples/caring/subject_manifest.yaml b/examples/caring/subject_manifest.yaml new file mode 100644 index 0000000..341f388 --- /dev/null +++ b/examples/caring/subject_manifest.yaml @@ -0,0 +1,22 @@ +id: subjects:tenant-alpha +subjects: + - id: user:alice + type: Human + display_name: Alice Example + organization_relation: Customer + roles: + - Doer + groups: + - group:platform-architecture + tenant: tenant:alpha +groups: + - id: group:platform-architecture + display_name: Platform Architecture + members: + - user:alice + tenant: tenant:alpha +tenants: + - id: tenant:alpha + name: Tenant Alpha +metadata: + source: examples/caring diff --git a/internal/policy/package_test.go b/internal/policy/package_test.go new file mode 100644 index 0000000..40c685e --- /dev/null +++ b/internal/policy/package_test.go @@ -0,0 +1,50 @@ +package policy_test + +import ( + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/netkingdom/flex-auth/pkg/api" +) + +func TestPolicyPackageMetadataParses(t *testing.T) { + var metadata api.PolicyPackageMetadata + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "policy_package.yaml"), &metadata) + + if metadata.Caring.Profile != api.CaringProfileCaring040RC2 { + t.Fatalf("metadata.Caring.Profile = %q; want %q", metadata.Caring.Profile, api.CaringProfileCaring040RC2) + } + if len(metadata.Caring.Capabilities) != 1 || metadata.Caring.Capabilities[0] != api.CapabilityView { + t.Errorf("metadata.Caring.Capabilities = %v; want [View]", metadata.Caring.Capabilities) + } + if len(metadata.Caring.Restrictions) != 1 || metadata.Caring.Restrictions[0] != api.RestrictionExportBlocked { + t.Errorf("metadata.Caring.Restrictions = %v; want [ExportBlocked]", metadata.Caring.Restrictions) + } +} + +func TestPolicyFixtureParses(t *testing.T) { + var fixture api.PolicyFixture + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "policy_fixture.yaml"), &fixture) + + if fixture.Expect.Effect != api.DecisionEffectAllow { + t.Errorf("fixture.Expect.Effect = %q; want allow", fixture.Expect.Effect) + } + if fixture.Request.CaringContext == nil { + t.Fatal("fixture.Request.CaringContext is nil") + } +} + +func loadYAML(t *testing.T, path string, out any) { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := yaml.Unmarshal(data, out); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } +} diff --git a/internal/registry/manifest_test.go b/internal/registry/manifest_test.go new file mode 100644 index 0000000..cc4ed86 --- /dev/null +++ b/internal/registry/manifest_test.go @@ -0,0 +1,43 @@ +package registry_test + +import ( + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/netkingdom/flex-auth/pkg/api" +) + +func TestRegistryManifestsParse(t *testing.T) { + var subjects api.SubjectManifest + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "subject_manifest.yaml"), &subjects) + if len(subjects.Subjects) != 1 { + t.Fatalf("Subjects len = %d; want 1", len(subjects.Subjects)) + } + if subjects.Subjects[0].Roles[0] != api.CanonicalRoleDoer { + t.Errorf("Subject role = %q; want Doer", subjects.Subjects[0].Roles[0]) + } + + var fact api.RelationshipFact + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "relationship_fact.yaml"), &fact) + if fact.Subject != "group:platform-architecture" || fact.Object != "document:internal-note" { + t.Fatalf("relationship fact did not parse as expected: %+v", fact) + } + if fact.Caring == nil || fact.Caring.Profile != api.CaringProfileCaring040RC2 { + t.Fatalf("fact.Caring = %+v; want CARING profile descriptor", fact.Caring) + } +} + +func loadYAML(t *testing.T, path string, out any) { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := yaml.Unmarshal(data, out); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } +} diff --git a/pkg/api/canonical.go b/pkg/api/canonical.go new file mode 100644 index 0000000..755609f --- /dev/null +++ b/pkg/api/canonical.go @@ -0,0 +1,249 @@ +package api + +// ProtectedSystemManifest describes a system that delegates authorization to +// flex-auth. +type ProtectedSystemManifest struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ResourceTypes []ResourceType `json:"resource_types,omitempty" yaml:"resource_types,omitempty"` + Actions []ActionDefinition `json:"actions,omitempty" yaml:"actions,omitempty"` + CaringProfiles []string `json:"caring_profiles,omitempty" yaml:"caring_profiles,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// ResourceType describes a resource namespace entry owned by a protected system. +type ResourceType struct { + Name string `json:"name" yaml:"name"` + ParentTypes []string `json:"parent_types,omitempty" yaml:"parent_types,omitempty"` + ScopeLevel ScopeLevel `json:"scope_level,omitempty" yaml:"scope_level,omitempty"` + Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// ActionDefinition maps a protected-system action to CARING capabilities. +type ActionDefinition struct { + Name string `json:"name" yaml:"name"` + Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` + Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// SubjectManifest declares subjects, groups, teams, and tenants for local +// registry loading. +type SubjectManifest struct { + ID string `json:"id" yaml:"id"` + Subjects []Subject `json:"subjects,omitempty" yaml:"subjects,omitempty"` + Groups []Group `json:"groups,omitempty" yaml:"groups,omitempty"` + Teams []Team `json:"teams,omitempty" yaml:"teams,omitempty"` + Tenants []Tenant `json:"tenants,omitempty" yaml:"tenants,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Subject is a human, service, automation, agent, or other acting identity. +type Subject struct { + ID string `json:"id" yaml:"id"` + Type SubjectType `json:"type" yaml:"type"` + DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"` + OrganizationRelation OrganizationRelation `json:"organization_relation,omitempty" yaml:"organization_relation,omitempty"` + Roles []CanonicalRole `json:"roles,omitempty" yaml:"roles,omitempty"` + Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + Claims map[string]any `json:"claims,omitempty" yaml:"claims,omitempty"` + CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Group is an assignment convenience, not a canonical role. +type Group struct { + ID string `json:"id" yaml:"id"` + DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"` + Members []string `json:"members,omitempty" yaml:"members,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Team is a group-like ownership unit used by protected systems. +type Team struct { + ID string `json:"id" yaml:"id"` + DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"` + Members []string `json:"members,omitempty" yaml:"members,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Tenant is a structural isolation boundary. +type Tenant struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// RelationshipFact records a relation between subjects, groups, teams, tenants, +// and resources. +type RelationshipFact struct { + ID string `json:"id" yaml:"id"` + System string `json:"system,omitempty" yaml:"system,omitempty"` + Subject string `json:"subject" yaml:"subject"` + Relation string `json:"relation" yaml:"relation"` + Object string `json:"object" yaml:"object"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"` + Caring *CaringAccessDescriptor `json:"caring,omitempty" yaml:"caring,omitempty"` + Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// PolicyPackageMetadata is the frontmatter contract for Rego-in-Markdown +// policy packages. +type PolicyPackageMetadata struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Version string `json:"version" yaml:"version"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + Package string `json:"package" yaml:"package"` + Caring CaringPolicyMetadata `json:"caring" yaml:"caring"` + Activation map[string]any `json:"activation,omitempty" yaml:"activation,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// CaringPolicyMetadata declares the CARING envelope a policy governs. +type CaringPolicyMetadata struct { + Profile string `json:"profile" yaml:"profile"` + CanonicalRoles []CanonicalRole `json:"canonical_roles,omitempty" yaml:"canonical_roles,omitempty"` + OrganizationRelations []OrganizationRelation `json:"organization_relations,omitempty" yaml:"organization_relations,omitempty"` + Scopes []CaringScope `json:"scopes,omitempty" yaml:"scopes,omitempty"` + Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"` + Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` + Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"` + Restrictions []Restriction `json:"restrictions,omitempty" yaml:"restrictions,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// PolicyFixture binds a check request to an expected decision. +type PolicyFixture struct { + ID string `json:"id" yaml:"id"` + Request CheckRequest `json:"request" yaml:"request"` + Expect DecisionExpectation `json:"expect" yaml:"expect"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// DecisionExpectation is the compact fixture expectation for policy tests. +type DecisionExpectation struct { + Effect DecisionEffect `json:"effect" yaml:"effect"` + Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` + Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"` + ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"` +} + +// CheckRequest is the stable protected-system-facing decision request. +type CheckRequest struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Subject SubjectRef `json:"subject" yaml:"subject"` + Action string `json:"action" yaml:"action"` + Resource ResourceRef `json:"resource" yaml:"resource"` + Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"` + CaringContext *CaringAccessDescriptor `json:"caring_context,omitempty" yaml:"caring_context,omitempty"` + PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` +} + +// BatchCheckRequest evaluates one subject/action against multiple resources. +type BatchCheckRequest struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Subject SubjectRef `json:"subject" yaml:"subject"` + Action string `json:"action" yaml:"action"` + Resources []ResourceRef `json:"resources" yaml:"resources"` + Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"` + PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` +} + +// SubjectRef is a normalized subject reference in request and decision shapes. +type SubjectRef struct { + ID string `json:"id" yaml:"id"` + Type SubjectType `json:"type,omitempty" yaml:"type,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` +} + +// ResourceRef is a normalized resource reference in request and decision shapes. +type ResourceRef struct { + ID string `json:"id" yaml:"id"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + System string `json:"system,omitempty" yaml:"system,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` +} + +// DecisionEffect is the stable decision outcome vocabulary. +type DecisionEffect string + +const ( + DecisionEffectAllow DecisionEffect = "allow" + DecisionEffectDeny DecisionEffect = "deny" + DecisionEffectRedact DecisionEffect = "redact" + DecisionEffectAuditOnly DecisionEffect = "audit_only" + DecisionEffectNotApplicable DecisionEffect = "not_applicable" +) + +// DecisionEnvelope is the stable response produced by standalone and delegated +// evaluators. +type DecisionEnvelope struct { + ID string `json:"id" yaml:"id"` + RequestID string `json:"request_id,omitempty" yaml:"request_id,omitempty"` + Effect DecisionEffect `json:"effect" yaml:"effect"` + Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` + MatchedPolicyVersion string `json:"matched_policy_version,omitempty" yaml:"matched_policy_version,omitempty"` + MatchedRule string `json:"matched_rule,omitempty" yaml:"matched_rule,omitempty"` + Resource ResourceRef `json:"resource" yaml:"resource"` + Subject SubjectRef `json:"subject" yaml:"subject"` + Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"` + Diagnostics map[string]any `json:"diagnostics,omitempty" yaml:"diagnostics,omitempty"` + Provenance DecisionProvenance `json:"provenance" yaml:"provenance"` + Caring *CaringDecisionMetadata `json:"caring,omitempty" yaml:"caring,omitempty"` +} + +// Obligation describes a follow-up behavior required by a decision. +type Obligation struct { + Type string `json:"type" yaml:"type"` + Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +// DecisionProvenance captures evaluator and policy provenance. +type DecisionProvenance struct { + Evaluator string `json:"evaluator" yaml:"evaluator"` + Mode string `json:"mode" yaml:"mode"` + PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` + PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` + DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` + DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` +} + +// CaringDecisionMetadata carries CARING descriptor and conformance details in +// a decision envelope. +type CaringDecisionMetadata struct { + Profile string `json:"profile" yaml:"profile"` + Descriptor *CaringAccessDescriptor `json:"descriptor,omitempty" yaml:"descriptor,omitempty"` + RestrictionsEvaluated []Restriction `json:"restrictions_evaluated,omitempty" yaml:"restrictions_evaluated,omitempty"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` + DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"` + ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"` + ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"` +} + +// AuditEvent is the local log shape for decisions and exposure events. +type AuditEvent struct { + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + DecisionID string `json:"decision_id,omitempty" yaml:"decision_id,omitempty"` + Subject SubjectRef `json:"subject" yaml:"subject"` + Resource ResourceRef `json:"resource,omitempty" yaml:"resource,omitempty"` + Action string `json:"action,omitempty" yaml:"action,omitempty"` + Effect DecisionEffect `json:"effect,omitempty" yaml:"effect,omitempty"` + Timestamp string `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` + ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} diff --git a/pkg/api/canonical_test.go b/pkg/api/canonical_test.go new file mode 100644 index 0000000..193c87b --- /dev/null +++ b/pkg/api/canonical_test.go @@ -0,0 +1,130 @@ +package api_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/netkingdom/flex-auth/pkg/api" +) + +func TestCaringAccessDescriptorExampleParses(t *testing.T) { + var got api.CaringAccessDescriptor + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "access_descriptor.yaml"), &got) + + if got.Profile != api.CaringProfileCaring040RC2 { + t.Fatalf("Profile = %q; want %q", got.Profile, api.CaringProfileCaring040RC2) + } + if got.CanonicalRole != api.CanonicalRoleDoer { + t.Errorf("CanonicalRole = %q; want Doer", got.CanonicalRole) + } + if got.Scope.Level != api.ScopeLevelResource || got.Scope.Tenant != "tenant:alpha" { + t.Errorf("Scope = %+v; want resource scope in tenant:alpha", got.Scope) + } + if len(got.Restrictions) != 1 || got.Restrictions[0] != api.RestrictionExportBlocked { + t.Errorf("Restrictions = %v; want [ExportBlocked]", got.Restrictions) + } +} + +func TestCheckRequestExampleParses(t *testing.T) { + var got api.CheckRequest + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "check_request.yaml"), &got) + + if got.Subject.ID != "user:alice" { + t.Errorf("Subject.ID = %q; want user:alice", got.Subject.ID) + } + if got.Resource.ID != "document:internal-note" { + t.Errorf("Resource.ID = %q; want document:internal-note", got.Resource.ID) + } + if got.CaringContext == nil { + t.Fatal("CaringContext is nil") + } + if got.CaringContext.AccessPath != api.AccessPathDirect { + t.Errorf("CaringContext.AccessPath = %q; want direct", got.CaringContext.AccessPath) + } +} + +func TestRegistryExamplesParse(t *testing.T) { + var subjects api.SubjectManifest + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "subject_manifest.yaml"), &subjects) + if len(subjects.Subjects) != 1 || subjects.Subjects[0].Type != api.SubjectTypeHuman { + t.Fatalf("subjects did not parse as expected: %+v", subjects.Subjects) + } + + var relationship api.RelationshipFact + loadYAML(t, filepath.Join("..", "..", "examples", "caring", "relationship_fact.yaml"), &relationship) + if relationship.Caring == nil { + t.Fatal("RelationshipFact.Caring is nil") + } + if relationship.Caring.SubjectType != api.SubjectTypeGroup { + t.Errorf("RelationshipFact.Caring.SubjectType = %q; want Group", relationship.Caring.SubjectType) + } +} + +func TestDecisionAndAuditExamplesParse(t *testing.T) { + var decision api.DecisionEnvelope + loadJSON(t, filepath.Join("..", "..", "examples", "caring", "decision_envelope.json"), &decision) + if decision.Effect != api.DecisionEffectAllow { + t.Errorf("Decision.Effect = %q; want allow", decision.Effect) + } + if decision.Caring == nil || decision.Caring.Profile != api.CaringProfileCaring040RC2 { + t.Fatalf("Decision.Caring = %+v; want CARING profile metadata", decision.Caring) + } + if len(decision.Caring.ConformanceFindings) != 1 { + t.Errorf("ConformanceFindings len = %d; want 1", len(decision.Caring.ConformanceFindings)) + } + + var audit api.AuditEvent + loadJSON(t, filepath.Join("..", "..", "examples", "caring", "audit_event.json"), &audit) + if audit.DecisionID != decision.ID { + t.Errorf("Audit.DecisionID = %q; want %q", audit.DecisionID, decision.ID) + } +} + +func TestSchemaFilesAreJSON(t *testing.T) { + schemaDir := filepath.Join("..", "..", "schemas") + entries, err := os.ReadDir(schemaDir) + if err != nil { + t.Fatalf("read schema dir: %v", err) + } + + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { + continue + } + t.Run(entry.Name(), func(t *testing.T) { + var got map[string]any + loadJSON(t, filepath.Join(schemaDir, entry.Name()), &got) + if got["$schema"] == "" || got["$id"] == "" { + t.Fatalf("%s missing $schema or $id", entry.Name()) + } + }) + } +} + +func loadYAML(t *testing.T, path string, out any) { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := yaml.Unmarshal(data, out); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } +} + +func loadJSON(t *testing.T, path string, out any) { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := json.Unmarshal(data, out); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } +} diff --git a/pkg/api/caring.go b/pkg/api/caring.go new file mode 100644 index 0000000..31b755c --- /dev/null +++ b/pkg/api/caring.go @@ -0,0 +1,335 @@ +package api + +// CaringProfileCaring040RC2 is the executable profile identifier for the +// CARING 0.4.0-RC2 standard pinned by flex-auth. +const CaringProfileCaring040RC2 = "caring-0.4.0-rc2" + +// SubjectType is the CARING subject dimension. +type SubjectType string + +const ( + SubjectTypeHuman SubjectType = "Human" + SubjectTypeGroup SubjectType = "Group" + SubjectTypeOrganization SubjectType = "Organization" + SubjectTypeService SubjectType = "Service" + SubjectTypeAutomation SubjectType = "Automation" + SubjectTypeAgent SubjectType = "Agent" + SubjectTypeSystem SubjectType = "System" + SubjectTypeDevice SubjectType = "Device" + SubjectTypeProcess SubjectType = "Process" + SubjectTypeAnonymous SubjectType = "Anonymous" + SubjectTypeUnknown SubjectType = "Unknown" +) + +// OrganizationRelation is the CARING organization-relation dimension. +type OrganizationRelation string + +const ( + OrganizationRelationVendor OrganizationRelation = "Vendor" + OrganizationRelationServiceProvider OrganizationRelation = "ServiceProvider" + OrganizationRelationDistributor OrganizationRelation = "Distributor" + OrganizationRelationConsultant OrganizationRelation = "Consultant" + OrganizationRelationCustomer OrganizationRelation = "Customer" + OrganizationRelationCommunity OrganizationRelation = "Community" + OrganizationRelationAuthority OrganizationRelation = "Authority" + OrganizationRelationUnknown OrganizationRelation = "Unknown" +) + +// CanonicalRole is the CARING lifecycle responsibility posture. +type CanonicalRole string + +const ( + CanonicalRoleCreator CanonicalRole = "Creator" + CanonicalRoleBuilder CanonicalRole = "Builder" + CanonicalRoleVerifier CanonicalRole = "Verifier" + CanonicalRoleMaintainer CanonicalRole = "Maintainer" + CanonicalRoleIntegrator CanonicalRole = "Integrator" + CanonicalRoleOperator CanonicalRole = "Operator" + CanonicalRoleManager CanonicalRole = "Manager" + CanonicalRoleCoach CanonicalRole = "Coach" + CanonicalRoleDoer CanonicalRole = "Doer" +) + +// ScopeLevel is the CARING scope ladder. +type ScopeLevel string + +const ( + ScopeLevelEcosystem ScopeLevel = "Ecosystem" + ScopeLevelProduct ScopeLevel = "Product" + ScopeLevelPlatform ScopeLevel = "Platform" + ScopeLevelCluster ScopeLevel = "Cluster" + ScopeLevelEnvironment ScopeLevel = "Environment" + ScopeLevelTenant ScopeLevel = "Tenant" + ScopeLevelNamespace ScopeLevel = "Namespace" + ScopeLevelDomain ScopeLevel = "Domain" + ScopeLevelWorkspace ScopeLevel = "Workspace" + ScopeLevelProject ScopeLevel = "Project" + ScopeLevelProcess ScopeLevel = "Process" + ScopeLevelDataset ScopeLevel = "Dataset" + ScopeLevelResource ScopeLevel = "Resource" + ScopeLevelSubresource ScopeLevel = "Subresource" + ScopeLevelRecord ScopeLevel = "Record" + ScopeLevelField ScopeLevel = "Field" + ScopeLevelAction ScopeLevel = "Action" +) + +// Plane is the CARING access-surface dimension. +type Plane string + +const ( + PlaneIntent Plane = "Intent" + PlaneBuild Plane = "Build" + PlaneRuntime Plane = "Runtime" + PlaneExecution Plane = "Execution" + PlaneConfiguration Plane = "Configuration" + PlaneData Plane = "Data" + PlaneIdentity Plane = "Identity" + PlanePolicy Plane = "Policy" + PlaneSecret Plane = "Secret" + PlaneAudit Plane = "Audit" + PlaneCommercial Plane = "Commercial" + PlaneCommunity Plane = "Community" +) + +// Capability is a CARING capability verb. +type Capability string + +const ( + CapabilityView Capability = "View" + CapabilityViewCollection Capability = "ViewCollection" + CapabilityObserve Capability = "Observe" + CapabilityCreate Capability = "Create" + CapabilityEditOwn Capability = "EditOwn" + CapabilityEditAssigned Capability = "EditAssigned" + CapabilityEditAny Capability = "EditAny" + CapabilityDeleteOwn Capability = "DeleteOwn" + CapabilityDeleteAny Capability = "DeleteAny" + CapabilityBulkDelete Capability = "BulkDelete" + CapabilitySubmit Capability = "Submit" + CapabilityComment Capability = "Comment" + CapabilityReview Capability = "Review" + CapabilityApprove Capability = "Approve" + CapabilityReject Capability = "Reject" + CapabilityPublish Capability = "Publish" + CapabilityArchive Capability = "Archive" + CapabilityRestore Capability = "Restore" + CapabilityExecute Capability = "Execute" + CapabilityConfigure Capability = "Configure" + CapabilityOperate Capability = "Operate" + CapabilityDeploy Capability = "Deploy" + CapabilityIntegrate Capability = "Integrate" + CapabilityGrant Capability = "Grant" + CapabilityRevoke Capability = "Revoke" + CapabilityDelegate Capability = "Delegate" + CapabilityImpersonate Capability = "Impersonate" + CapabilityExport Capability = "Export" + CapabilityImport Capability = "Import" + CapabilityReplicate Capability = "Replicate" + CapabilityEncrypt Capability = "Encrypt" + CapabilityDecrypt Capability = "Decrypt" + CapabilityMask Capability = "Mask" + CapabilityInspect Capability = "Inspect" + CapabilityAudit Capability = "Audit" + CapabilityOverride Capability = "Override" + CapabilityEscalate Capability = "Escalate" + CapabilityBind Capability = "Bind" + CapabilityUse Capability = "Use" +) + +// ExposureMode describes how much information becomes visible or extractable. +type ExposureMode string + +const ( + ExposureModeNone ExposureMode = "None" + ExposureModeMetadata ExposureMode = "Metadata" + ExposureModeMasked ExposureMode = "Masked" + ExposureModeAggregated ExposureMode = "Aggregated" + ExposureModeSynthetic ExposureMode = "Synthetic" + ExposureModePseudonymous ExposureMode = "Pseudonymous" + ExposureModeEncrypted ExposureMode = "Encrypted" + ExposureModePlaintext ExposureMode = "Plaintext" + ExposureModeSecretMaterial ExposureMode = "SecretMaterial" + ExposureModeExportable ExposureMode = "Exportable" + ExposureModeCrossTenantAggregate ExposureMode = "CrossTenantAggregate" +) + +// Condition is a CARING runtime or governance condition. +type Condition string + +const ( + ConditionMFARequired Condition = "MFARequired" + ConditionDeviceTrusted Condition = "DeviceTrusted" + ConditionNetworkTrusted Condition = "NetworkTrusted" + ConditionTicketRequired Condition = "TicketRequired" + ConditionTenantConsentRequired Condition = "TenantConsentRequired" + ConditionCustomerApprovalRequired Condition = "CustomerApprovalRequired" + ConditionDualApprovalRequired Condition = "DualApprovalRequired" + ConditionTimeLimited Condition = "TimeLimited" + ConditionBusinessHoursOnly Condition = "BusinessHoursOnly" + ConditionEmergencyOnly Condition = "EmergencyOnly" + ConditionTrainingRequired Condition = "TrainingRequired" + ConditionContractRequired Condition = "ContractRequired" + ConditionNDARequired Condition = "NDARequired" + ConditionPurposeBound Condition = "PurposeBound" + ConditionCaseBound Condition = "CaseBound" + ConditionEnvironmentBound Condition = "EnvironmentBound" + ConditionNamespaceBound Condition = "NamespaceBound" + ConditionPipelineBound Condition = "PipelineBound" + ConditionChangeWindowBound Condition = "ChangeWindowBound" + ConditionLogged Condition = "Logged" + ConditionRecorded Condition = "Recorded" + ConditionNotificationRequired Condition = "NotificationRequired" + ConditionPostReviewRequired Condition = "PostReviewRequired" + ConditionHumanReviewRequired Condition = "HumanReviewRequired" + ConditionPolicyReviewRequired Condition = "PolicyReviewRequired" + ConditionWorkloadIdentityRequired Condition = "WorkloadIdentityRequired" +) + +// LifecycleState describes why access exists now. +type LifecycleState string + +const ( + LifecycleStateDesign LifecycleState = "Design" + LifecycleStateBuild LifecycleState = "Build" + LifecycleStateTest LifecycleState = "Test" + LifecycleStateReview LifecycleState = "Review" + LifecycleStateRelease LifecycleState = "Release" + LifecycleStateOnboard LifecycleState = "Onboard" + LifecycleStateIntegrate LifecycleState = "Integrate" + LifecycleStateMigrate LifecycleState = "Migrate" + LifecycleStateOperate LifecycleState = "Operate" + LifecycleStateSupport LifecycleState = "Support" + LifecycleStateImprove LifecycleState = "Improve" + LifecycleStateDeprecate LifecycleState = "Deprecate" + LifecycleStateArchive LifecycleState = "Archive" + LifecycleStateIncident LifecycleState = "Incident" + LifecycleStateLegal LifecycleState = "Legal" + LifecycleStateTerminate LifecycleState = "Terminate" +) + +// Restriction is an overriding CARING deny or limiting policy effect. +type Restriction string + +const ( + RestrictionNoAccess Restriction = "NoAccess" + RestrictionSuspended Restriction = "Suspended" + RestrictionTerminated Restriction = "Terminated" + RestrictionQuarantined Restriction = "Quarantined" + RestrictionScopeExcluded Restriction = "ScopeExcluded" + RestrictionDataClassRestricted Restriction = "DataClassRestricted" + RestrictionLegalHold Restriction = "LegalHold" + RestrictionExportBlocked Restriction = "ExportBlocked" + RestrictionImpersonationBlocked Restriction = "ImpersonationBlocked" + RestrictionCrossTenantBlocked Restriction = "CrossTenantBlocked" + RestrictionSecretAccessBlocked Restriction = "SecretAccessBlocked" + RestrictionPolicyFrozen Restriction = "PolicyFrozen" + RestrictionEmergencyLocked Restriction = "EmergencyLocked" + RestrictionRiskDenied Restriction = "RiskDenied" + RestrictionExecutionBlocked Restriction = "ExecutionBlocked" + RestrictionWorkloadCreationBlocked Restriction = "WorkloadCreationBlocked" + RestrictionPrivilegeEscalationBlocked Restriction = "PrivilegeEscalationBlocked" +) + +// ExposureEventType is a CARING exceptional or irregular access class. +type ExposureEventType string + +const ( + ExposureEventSupport ExposureEventType = "X-Support" + ExposureEventBreakGlass ExposureEventType = "X-BreakGlass" + ExposureEventSecurityTest ExposureEventType = "X-SecurityTest" + ExposureEventIncident ExposureEventType = "X-Incident" + ExposureEventLegalDemand ExposureEventType = "X-LegalDemand" + ExposureEventComplianceAudit ExposureEventType = "X-ComplianceAudit" + ExposureEventMigration ExposureEventType = "X-Migration" + ExposureEventRecovery ExposureEventType = "X-Recovery" + ExposureEventAdversarial ExposureEventType = "X-Adversarial" + ExposureEventMisconfiguration ExposureEventType = "X-Misconfiguration" + ExposureEventInducedAccess ExposureEventType = "X-InducedAccess" + ExposureEventPrivilegeEscalation ExposureEventType = "X-PrivilegeEscalation" +) + +// AccessPath describes how access is exercised. +type AccessPath string + +const ( + AccessPathDirect AccessPath = "direct" + AccessPathDelegated AccessPath = "delegated" + AccessPathMediated AccessPath = "mediated" + AccessPathInduced AccessPath = "induced" +) + +// CaringScope identifies where a CARING descriptor applies. +type CaringScope struct { + Level ScopeLevel `json:"level" yaml:"level"` + ID string `json:"id" yaml:"id"` + Parent string `json:"parent,omitempty" yaml:"parent,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` + Resource string `json:"resource,omitempty" yaml:"resource,omitempty"` + Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` +} + +// CaringAccessDescriptor is the executable flex-auth representation of a +// CARING access assignment. +type CaringAccessDescriptor struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Profile string `json:"profile" yaml:"profile"` + SubjectType SubjectType `json:"subject_type" yaml:"subject_type"` + OrganizationRelation OrganizationRelation `json:"organization_relation" yaml:"organization_relation"` + CanonicalRole CanonicalRole `json:"canonical_role" yaml:"canonical_role"` + Scope CaringScope `json:"scope" yaml:"scope"` + Planes []Plane `json:"planes" yaml:"planes"` + Capabilities []Capability `json:"capabilities" yaml:"capabilities"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` + Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"` + LifecycleState LifecycleState `json:"lifecycle_state,omitempty" yaml:"lifecycle_state,omitempty"` + Restrictions []Restriction `json:"restrictions,omitempty" yaml:"restrictions,omitempty"` + ExposureEvent ExposureEventType `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"` + DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"` + AccessPath AccessPath `json:"access_path,omitempty" yaml:"access_path,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// CaringDerivedCapability records effective authority created by another grant. +type CaringDerivedCapability struct { + Capability Capability `json:"capability" yaml:"capability"` + Reason string `json:"reason" yaml:"reason"` + Source string `json:"source,omitempty" yaml:"source,omitempty"` + Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` +} + +// CaringConformanceFinding is a diagnostic emitted by descriptive or +// prescriptive CARING validation. +type CaringConformanceFinding struct { + Code string `json:"code" yaml:"code"` + Severity string `json:"severity" yaml:"severity"` + Message string `json:"message" yaml:"message"` + Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"` + Descriptor string `json:"descriptor,omitempty" yaml:"descriptor,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// CaringExposureEvent records exceptional or irregular information exposure. +type CaringExposureEvent struct { + ID string `json:"id" yaml:"id"` + Type ExposureEventType `json:"type" yaml:"type"` + Actor string `json:"actor" yaml:"actor"` + Subject string `json:"subject" yaml:"subject"` + Descriptor *CaringAccessDescriptor `json:"descriptor,omitempty" yaml:"descriptor,omitempty"` + Scope *CaringScope `json:"scope,omitempty" yaml:"scope,omitempty"` + Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"` + CapabilitiesUsed []Capability `json:"capabilities_used,omitempty" yaml:"capabilities_used,omitempty"` + DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"` + ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"` + Reason string `json:"reason" yaml:"reason"` + AuthoritySource string `json:"authority_source,omitempty" yaml:"authority_source,omitempty"` + Approval string `json:"approval,omitempty" yaml:"approval,omitempty"` + StartTime string `json:"start_time,omitempty" yaml:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty" yaml:"end_time,omitempty"` + ResourcesAccessed []string `json:"resources_accessed,omitempty" yaml:"resources_accessed,omitempty"` + Evidence []string `json:"evidence,omitempty" yaml:"evidence,omitempty"` + NotificationStatus string `json:"notification_status,omitempty" yaml:"notification_status,omitempty"` + PostReview string `json:"post_review,omitempty" yaml:"post_review,omitempty"` + ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} diff --git a/pkg/api/resource_manifest.go b/pkg/api/resource_manifest.go index 0a8d38c..4fb6154 100644 --- a/pkg/api/resource_manifest.go +++ b/pkg/api/resource_manifest.go @@ -6,23 +6,25 @@ package api // schemas/resource_manifest.schema.json for the JSON Schema and // examples/markitect/resource_manifest.yaml for the canonical example. type ResourceManifest struct { - ID string `json:"id" yaml:"id"` - System string `json:"system" yaml:"system"` - Resources []Resource `json:"resources" yaml:"resources"` - Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` - Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` + ID string `json:"id" yaml:"id"` + System string `json:"system" yaml:"system"` + Resources []Resource `json:"resources" yaml:"resources"` + Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` + CaringProfile string `json:"caring_profile,omitempty" yaml:"caring_profile,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` } // Resource is one entry in a ResourceManifest. type Resource struct { - ID string `json:"id" yaml:"id"` - Type string `json:"type" yaml:"type"` - Path string `json:"path,omitempty" yaml:"path,omitempty"` - Parent string `json:"parent,omitempty" yaml:"parent,omitempty"` - Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"` - TrustZone string `json:"trust_zone,omitempty" yaml:"trust_zone,omitempty"` - Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` - Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Parent string `json:"parent,omitempty" yaml:"parent,omitempty"` + Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"` + TrustZone string `json:"trust_zone,omitempty" yaml:"trust_zone,omitempty"` + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` + Caring *CaringAccessDescriptor `json:"caring,omitempty" yaml:"caring,omitempty"` + Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` } // FlexAuthContractV0 is the metadata.flex_auth_contract value that diff --git a/schemas/README.md b/schemas/README.md index 7bce761..0916994 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -3,9 +3,13 @@ JSON Schema definitions for flex-auth's canonical artefacts: - `resource_manifest.schema.json` (pinned in `FLEX-WP-0005 P5.3`) +- `protected_system_manifest.schema.json` - `subject_manifest.schema.json` - `relationship_fact.schema.json` -- `policy_package_frontmatter.schema.json` +- `caring_profile.schema.json` +- `caring_access_descriptor.schema.json` +- `policy_package.schema.json` +- `policy_fixture.schema.json` - `check_request.schema.json` - `decision_envelope.schema.json` - `audit_event.schema.json` diff --git a/schemas/audit_event.schema.json b/schemas/audit_event.schema.json new file mode 100644 index 0000000..3e57584 --- /dev/null +++ b/schemas/audit_event.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/audit_event.schema.json", + "title": "AuditEvent", + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "subject"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"type": "string", "minLength": 1}, + "decision_id": {"type": "string", "minLength": 1}, + "subject": {"$ref": "https://flex-auth.netkingdom/schemas/check_request.schema.json#/$defs/subject_ref"}, + "resource": {"$ref": "https://flex-auth.netkingdom/schemas/check_request.schema.json#/$defs/resource_ref"}, + "action": {"type": "string", "minLength": 1}, + "effect": {"enum": ["allow", "deny", "redact", "audit_only", "not_applicable"]}, + "timestamp": {"type": "string", "minLength": 1}, + "exposure_event": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/exposure_event"}, + "metadata": {"type": "object", "additionalProperties": true} + } +} diff --git a/schemas/caring_access_descriptor.schema.json b/schemas/caring_access_descriptor.schema.json new file mode 100644 index 0000000..6c4818e --- /dev/null +++ b/schemas/caring_access_descriptor.schema.json @@ -0,0 +1,358 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json", + "title": "CaringAccessDescriptor", + "description": "Executable flex-auth representation of a CARING access descriptor pinned to CARING 0.4.0-RC2.", + "type": "object", + "additionalProperties": false, + "required": [ + "profile", + "subject_type", + "organization_relation", + "canonical_role", + "scope", + "planes", + "capabilities" + ], + "properties": { + "id": {"type": "string", "minLength": 1}, + "profile": {"const": "caring-0.4.0-rc2"}, + "subject_type": {"$ref": "#/$defs/subject_type"}, + "organization_relation": {"$ref": "#/$defs/organization_relation"}, + "canonical_role": {"$ref": "#/$defs/canonical_role"}, + "scope": {"$ref": "#/$defs/scope"}, + "planes": { + "type": "array", + "items": {"$ref": "#/$defs/plane"}, + "minItems": 1, + "uniqueItems": true + }, + "capabilities": { + "type": "array", + "items": {"$ref": "#/$defs/capability"}, + "minItems": 1, + "uniqueItems": true + }, + "exposure_modes": { + "type": "array", + "items": {"$ref": "#/$defs/exposure_mode"}, + "uniqueItems": true + }, + "conditions": { + "type": "array", + "items": {"$ref": "#/$defs/condition"}, + "uniqueItems": true + }, + "lifecycle_state": {"$ref": "#/$defs/lifecycle_state"}, + "restrictions": { + "type": "array", + "items": {"$ref": "#/$defs/restriction"}, + "uniqueItems": true + }, + "exposure_event": {"$ref": "#/$defs/exposure_event_type"}, + "derived_capabilities": { + "type": "array", + "items": {"$ref": "#/$defs/derived_capability"} + }, + "access_path": {"enum": ["direct", "delegated", "mediated", "induced"]}, + "metadata": {"type": "object", "additionalProperties": true} + }, + "$defs": { + "subject_type": { + "enum": [ + "Human", + "Group", + "Organization", + "Service", + "Automation", + "Agent", + "System", + "Device", + "Process", + "Anonymous", + "Unknown" + ] + }, + "organization_relation": { + "enum": [ + "Vendor", + "ServiceProvider", + "Distributor", + "Consultant", + "Customer", + "Community", + "Authority", + "Unknown" + ] + }, + "canonical_role": { + "enum": [ + "Creator", + "Builder", + "Verifier", + "Maintainer", + "Integrator", + "Operator", + "Manager", + "Coach", + "Doer" + ] + }, + "scope_level": { + "enum": [ + "Ecosystem", + "Product", + "Platform", + "Cluster", + "Environment", + "Tenant", + "Namespace", + "Domain", + "Workspace", + "Project", + "Process", + "Dataset", + "Resource", + "Subresource", + "Record", + "Field", + "Action" + ] + }, + "scope": { + "type": "object", + "additionalProperties": false, + "required": ["level", "id"], + "properties": { + "level": {"$ref": "#/$defs/scope_level"}, + "id": {"type": "string", "minLength": 1}, + "parent": {"type": "string", "minLength": 1}, + "tenant": {"type": "string", "minLength": 1}, + "resource": {"type": "string", "minLength": 1}, + "attributes": {"type": "object", "additionalProperties": true} + } + }, + "plane": { + "enum": [ + "Intent", + "Build", + "Runtime", + "Execution", + "Configuration", + "Data", + "Identity", + "Policy", + "Secret", + "Audit", + "Commercial", + "Community" + ] + }, + "capability": { + "enum": [ + "View", + "ViewCollection", + "Observe", + "Create", + "EditOwn", + "EditAssigned", + "EditAny", + "DeleteOwn", + "DeleteAny", + "BulkDelete", + "Submit", + "Comment", + "Review", + "Approve", + "Reject", + "Publish", + "Archive", + "Restore", + "Execute", + "Configure", + "Operate", + "Deploy", + "Integrate", + "Grant", + "Revoke", + "Delegate", + "Impersonate", + "Export", + "Import", + "Replicate", + "Encrypt", + "Decrypt", + "Mask", + "Inspect", + "Audit", + "Override", + "Escalate", + "Bind", + "Use" + ] + }, + "exposure_mode": { + "enum": [ + "None", + "Metadata", + "Masked", + "Aggregated", + "Synthetic", + "Pseudonymous", + "Encrypted", + "Plaintext", + "SecretMaterial", + "Exportable", + "CrossTenantAggregate" + ] + }, + "condition": { + "enum": [ + "MFARequired", + "DeviceTrusted", + "NetworkTrusted", + "TicketRequired", + "TenantConsentRequired", + "CustomerApprovalRequired", + "DualApprovalRequired", + "TimeLimited", + "BusinessHoursOnly", + "EmergencyOnly", + "TrainingRequired", + "ContractRequired", + "NDARequired", + "PurposeBound", + "CaseBound", + "EnvironmentBound", + "NamespaceBound", + "PipelineBound", + "ChangeWindowBound", + "Logged", + "Recorded", + "NotificationRequired", + "PostReviewRequired", + "HumanReviewRequired", + "PolicyReviewRequired", + "WorkloadIdentityRequired" + ] + }, + "lifecycle_state": { + "enum": [ + "Design", + "Build", + "Test", + "Review", + "Release", + "Onboard", + "Integrate", + "Migrate", + "Operate", + "Support", + "Improve", + "Deprecate", + "Archive", + "Incident", + "Legal", + "Terminate" + ] + }, + "restriction": { + "enum": [ + "NoAccess", + "Suspended", + "Terminated", + "Quarantined", + "ScopeExcluded", + "DataClassRestricted", + "LegalHold", + "ExportBlocked", + "ImpersonationBlocked", + "CrossTenantBlocked", + "SecretAccessBlocked", + "PolicyFrozen", + "EmergencyLocked", + "RiskDenied", + "ExecutionBlocked", + "WorkloadCreationBlocked", + "PrivilegeEscalationBlocked" + ] + }, + "exposure_event_type": { + "enum": [ + "X-Support", + "X-BreakGlass", + "X-SecurityTest", + "X-Incident", + "X-LegalDemand", + "X-ComplianceAudit", + "X-Migration", + "X-Recovery", + "X-Adversarial", + "X-Misconfiguration", + "X-InducedAccess", + "X-PrivilegeEscalation" + ] + }, + "derived_capability": { + "type": "object", + "additionalProperties": false, + "required": ["capability", "reason"], + "properties": { + "capability": {"$ref": "#/$defs/capability"}, + "reason": {"type": "string", "minLength": 1}, + "source": {"type": "string", "minLength": 1}, + "planes": { + "type": "array", + "items": {"$ref": "#/$defs/plane"}, + "uniqueItems": true + }, + "exposure_modes": { + "type": "array", + "items": {"$ref": "#/$defs/exposure_mode"}, + "uniqueItems": true + } + } + }, + "conformance_finding": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "message"], + "properties": { + "code": {"type": "string", "minLength": 1}, + "severity": {"enum": ["info", "warning", "violation", "blocked"]}, + "message": {"type": "string", "minLength": 1}, + "fields": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "descriptor": {"type": "string", "minLength": 1}, + "metadata": {"type": "object", "additionalProperties": true} + } + }, + "exposure_event": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "actor", "subject", "reason"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"$ref": "#/$defs/exposure_event_type"}, + "actor": {"type": "string", "minLength": 1}, + "subject": {"type": "string", "minLength": 1}, + "descriptor": {"$ref": "#"}, + "scope": {"$ref": "#/$defs/scope"}, + "planes": {"type": "array", "items": {"$ref": "#/$defs/plane"}}, + "capabilities_used": {"type": "array", "items": {"$ref": "#/$defs/capability"}}, + "derived_capabilities": {"type": "array", "items": {"$ref": "#/$defs/derived_capability"}}, + "exposure_modes": {"type": "array", "items": {"$ref": "#/$defs/exposure_mode"}}, + "reason": {"type": "string", "minLength": 1}, + "authority_source": {"type": "string", "minLength": 1}, + "approval": {"type": "string", "minLength": 1}, + "start_time": {"type": "string", "minLength": 1}, + "end_time": {"type": "string", "minLength": 1}, + "resources_accessed": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "evidence": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "notification_status": {"type": "string", "minLength": 1}, + "post_review": {"type": "string", "minLength": 1}, + "conformance_findings": {"type": "array", "items": {"$ref": "#/$defs/conformance_finding"}}, + "metadata": {"type": "object", "additionalProperties": true} + } + } + } +} diff --git a/schemas/caring_profile.schema.json b/schemas/caring_profile.schema.json new file mode 100644 index 0000000..31e5256 --- /dev/null +++ b/schemas/caring_profile.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/caring_profile.schema.json", + "title": "CaringProfile", + "description": "Machine-readable pin for a CARING profile supported by flex-auth.", + "type": "object", + "additionalProperties": false, + "required": ["id", "standard", "version"], + "properties": { + "id": {"const": "caring-0.4.0-rc2"}, + "standard": {"const": "CARING"}, + "version": {"const": "0.4.0-RC2"}, + "source": {"type": "string", "minLength": 1}, + "metadata": {"type": "object", "additionalProperties": true} + } +} diff --git a/schemas/check_request.schema.json b/schemas/check_request.schema.json new file mode 100644 index 0000000..11f8c9f --- /dev/null +++ b/schemas/check_request.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/check_request.schema.json", + "title": "CheckRequest", + "type": "object", + "additionalProperties": false, + "required": ["subject", "action", "resource"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "subject": {"$ref": "#/$defs/subject_ref"}, + "action": {"type": "string", "minLength": 1}, + "resource": {"$ref": "#/$defs/resource_ref"}, + "context": {"type": "object", "additionalProperties": true}, + "caring_context": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json"}, + "policy_version": {"type": "string", "minLength": 1} + }, + "$defs": { + "subject_ref": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/subject_type"}, + "tenant": {"type": "string", "minLength": 1}, + "attributes": {"type": "object", "additionalProperties": true} + } + }, + "resource_ref": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"type": "string", "minLength": 1}, + "system": {"type": "string", "minLength": 1}, + "tenant": {"type": "string", "minLength": 1}, + "attributes": {"type": "object", "additionalProperties": true} + } + } + } +} diff --git a/schemas/decision_envelope.schema.json b/schemas/decision_envelope.schema.json new file mode 100644 index 0000000..ce837dc --- /dev/null +++ b/schemas/decision_envelope.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/decision_envelope.schema.json", + "title": "DecisionEnvelope", + "type": "object", + "additionalProperties": false, + "required": ["id", "effect", "resource", "subject", "provenance"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "request_id": {"type": "string", "minLength": 1}, + "effect": {"enum": ["allow", "deny", "redact", "audit_only", "not_applicable"]}, + "reason": {"type": "string"}, + "matched_policy_version": {"type": "string", "minLength": 1}, + "matched_rule": {"type": "string", "minLength": 1}, + "resource": {"$ref": "https://flex-auth.netkingdom/schemas/check_request.schema.json#/$defs/resource_ref"}, + "subject": {"$ref": "https://flex-auth.netkingdom/schemas/check_request.schema.json#/$defs/subject_ref"}, + "obligations": {"type": "array", "items": {"$ref": "#/$defs/obligation"}}, + "diagnostics": {"type": "object", "additionalProperties": true}, + "provenance": {"$ref": "#/$defs/provenance"}, + "caring": {"$ref": "#/$defs/caring_decision_metadata"} + }, + "$defs": { + "obligation": { + "type": "object", + "additionalProperties": false, + "required": ["type"], + "properties": { + "type": {"type": "string", "minLength": 1}, + "parameters": {"type": "object", "additionalProperties": true} + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["evaluator", "mode"], + "properties": { + "evaluator": {"type": "string", "minLength": 1}, + "mode": {"type": "string", "minLength": 1}, + "policy_package": {"type": "string", "minLength": 1}, + "policy_version": {"type": "string", "minLength": 1}, + "directory_etag": {"type": "string", "minLength": 1}, + "decision_time": {"type": "string", "minLength": 1} + } + }, + "caring_decision_metadata": { + "type": "object", + "additionalProperties": false, + "required": ["profile"], + "properties": { + "profile": {"const": "caring-0.4.0-rc2"}, + "descriptor": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json"}, + "restrictions_evaluated": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/restriction"}, + "uniqueItems": true + }, + "exposure_modes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/exposure_mode"}, + "uniqueItems": true + }, + "derived_capabilities": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/derived_capability"} + }, + "conformance_findings": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/conformance_finding"} + }, + "exposure_event": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/exposure_event"} + } + } + } +} diff --git a/schemas/policy_fixture.schema.json b/schemas/policy_fixture.schema.json new file mode 100644 index 0000000..18809e1 --- /dev/null +++ b/schemas/policy_fixture.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/policy_fixture.schema.json", + "title": "PolicyFixture", + "type": "object", + "additionalProperties": false, + "required": ["id", "request", "expect"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "request": {"$ref": "https://flex-auth.netkingdom/schemas/check_request.schema.json"}, + "expect": {"$ref": "#/$defs/decision_expectation"}, + "metadata": {"type": "object", "additionalProperties": true} + }, + "$defs": { + "decision_expectation": { + "type": "object", + "additionalProperties": false, + "required": ["effect"], + "properties": { + "effect": {"enum": ["allow", "deny", "redact", "audit_only", "not_applicable"]}, + "reason": {"type": "string", "minLength": 1}, + "obligations": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/decision_envelope.schema.json#/$defs/obligation"} + }, + "conformance_findings": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/conformance_finding"} + } + } + } + } +} diff --git a/schemas/policy_package.schema.json b/schemas/policy_package.schema.json new file mode 100644 index 0000000..a850fd9 --- /dev/null +++ b/schemas/policy_package.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/policy_package.schema.json", + "title": "PolicyPackageMetadata", + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "package", "caring"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "version": {"type": "string", "minLength": 1}, + "status": {"type": "string", "minLength": 1}, + "package": {"type": "string", "minLength": 1}, + "caring": {"$ref": "#/$defs/caring_policy_metadata"}, + "activation": {"type": "object", "additionalProperties": true}, + "metadata": {"type": "object", "additionalProperties": true} + }, + "$defs": { + "caring_policy_metadata": { + "type": "object", + "additionalProperties": false, + "required": ["profile"], + "properties": { + "profile": {"const": "caring-0.4.0-rc2"}, + "canonical_roles": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/canonical_role"}, + "uniqueItems": true + }, + "organization_relations": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/organization_relation"}, + "uniqueItems": true + }, + "scopes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/scope"} + }, + "planes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/plane"}, + "uniqueItems": true + }, + "capabilities": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/capability"}, + "uniqueItems": true + }, + "exposure_modes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/exposure_mode"}, + "uniqueItems": true + }, + "conditions": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/condition"}, + "uniqueItems": true + }, + "restrictions": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/restriction"}, + "uniqueItems": true + }, + "metadata": {"type": "object", "additionalProperties": true} + } + } + } +} diff --git a/schemas/protected_system_manifest.schema.json b/schemas/protected_system_manifest.schema.json new file mode 100644 index 0000000..c2e3910 --- /dev/null +++ b/schemas/protected_system_manifest.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/protected_system_manifest.schema.json", + "title": "ProtectedSystemManifest", + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "description": {"type": "string"}, + "resource_types": { + "type": "array", + "items": {"$ref": "#/$defs/resource_type"} + }, + "actions": { + "type": "array", + "items": {"$ref": "#/$defs/action"} + }, + "caring_profiles": { + "type": "array", + "items": {"const": "caring-0.4.0-rc2"}, + "uniqueItems": true + }, + "metadata": {"type": "object", "additionalProperties": true} + }, + "$defs": { + "resource_type": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "parent_types": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "scope_level": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/scope_level"}, + "planes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/plane"}, + "uniqueItems": true + }, + "metadata": {"type": "object", "additionalProperties": true} + } + }, + "action": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "capabilities": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/capability"}, + "uniqueItems": true + }, + "planes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/plane"}, + "uniqueItems": true + }, + "exposure_modes": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/exposure_mode"}, + "uniqueItems": true + }, + "metadata": {"type": "object", "additionalProperties": true} + } + } + } +} diff --git a/schemas/relationship_fact.schema.json b/schemas/relationship_fact.schema.json new file mode 100644 index 0000000..e043d23 --- /dev/null +++ b/schemas/relationship_fact.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/relationship_fact.schema.json", + "title": "RelationshipFact", + "type": "object", + "additionalProperties": false, + "required": ["id", "subject", "relation", "object"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "system": {"type": "string", "minLength": 1}, + "subject": {"type": "string", "minLength": 1}, + "relation": {"type": "string", "minLength": 1}, + "object": {"type": "string", "minLength": 1}, + "tenant": {"type": "string", "minLength": 1}, + "conditions": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/condition"}, + "uniqueItems": true + }, + "caring": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json"}, + "provenance": {"type": "object", "additionalProperties": true}, + "metadata": {"type": "object", "additionalProperties": true} + } +} diff --git a/schemas/resource_manifest.schema.json b/schemas/resource_manifest.schema.json index 964af7f..49ca3e8 100644 --- a/schemas/resource_manifest.schema.json +++ b/schemas/resource_manifest.schema.json @@ -28,6 +28,11 @@ "items": {"type": "string", "minLength": 1}, "uniqueItems": true }, + "caring_profile": { + "type": "string", + "description": "Optional CARING profile identifier used by resource-level descriptors.", + "const": "caring-0.4.0-rc2" + }, "metadata": { "type": "object", "description": "Free-form provenance and contract metadata. Conventions: 'source' (origin description), 'flex_auth_contract' (contract version string, currently 'resource-registration-v0').", @@ -76,6 +81,10 @@ "description": "Owner identifier, conventionally 'team:' or 'user:'.", "minLength": 1 }, + "caring": { + "description": "Optional CARING descriptor for this resource. Policy packages may require this field for conformance checks.", + "$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json" + }, "attributes": { "type": "object", "description": "Free-form attributes that policy packages may consult. Reserved keys may be defined by individual policy packages.", diff --git a/schemas/subject_manifest.schema.json b/schemas/subject_manifest.schema.json new file mode 100644 index 0000000..abb6143 --- /dev/null +++ b/schemas/subject_manifest.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/subject_manifest.schema.json", + "title": "SubjectManifest", + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "subjects": {"type": "array", "items": {"$ref": "#/$defs/subject"}}, + "groups": {"type": "array", "items": {"$ref": "#/$defs/group"}}, + "teams": {"type": "array", "items": {"$ref": "#/$defs/group"}}, + "tenants": {"type": "array", "items": {"$ref": "#/$defs/tenant"}}, + "metadata": {"type": "object", "additionalProperties": true} + }, + "$defs": { + "subject": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/subject_type"}, + "display_name": {"type": "string", "minLength": 1}, + "organization_relation": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/organization_relation"}, + "roles": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/canonical_role"}, + "uniqueItems": true + }, + "groups": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "tenant": {"type": "string", "minLength": 1}, + "claims": {"type": "object", "additionalProperties": true}, + "caring_descriptors": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json"} + }, + "metadata": {"type": "object", "additionalProperties": true} + } + }, + "group": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "display_name": {"type": "string", "minLength": 1}, + "members": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "tenant": {"type": "string", "minLength": 1}, + "caring_descriptors": { + "type": "array", + "items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json"} + }, + "metadata": {"type": "object", "additionalProperties": true} + } + }, + "tenant": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "metadata": {"type": "object", "additionalProperties": true} + } + } + } +} diff --git a/workplans/FLEX-WP-0002-standalone-policy-as-code-core.md b/workplans/FLEX-WP-0002-standalone-policy-as-code-core.md index aa2c8d6..ab78d1c 100644 --- a/workplans/FLEX-WP-0002-standalone-policy-as-code-core.md +++ b/workplans/FLEX-WP-0002-standalone-policy-as-code-core.md @@ -67,7 +67,7 @@ Backends may change later, but these envelopes must stay stable: ```task id: FLEX-WP-0002-T001 -status: todo +status: done priority: high state_hub_task_id: "534e5251-8529-48fe-8cf8-b3b6bc4ec1f4" ```