package api import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "time" ) // 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"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` Version string `json:"version" yaml:"version"` Status string `json:"status,omitempty" yaml:"status,omitempty"` Package string `json:"package" yaml:"package"` Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` Fixtures []string `json:"fixtures,omitempty" yaml:"fixtures,omitempty"` 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"` // AllowTTL is a Go duration (for example "15m") that bounds every allow // this package produces. Omit to use DefaultAllowTTL. "none" or "0s" // means no stated end; the engine denies those allows (§9.7.1). AllowTTL string `json:"allow_ttl,omitempty" yaml:"allow_ttl,omitempty"` } // CaringPolicyMetadata declares the CARING envelope a policy governs. type CaringPolicyMetadata struct { Profile string `json:"profile" yaml:"profile"` Enforce bool `json:"enforce,omitempty" yaml:"enforce,omitempty"` 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"` Tenant string `json:"tenant,omitempty" yaml:"tenant,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"` Tenant string `json:"tenant,omitempty" yaml:"tenant,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" ) // DecisionRecordContractV1 is the published decision-record contract identifier. const DecisionRecordContractV1 = "flex-auth.decision-record.v1" // DecisionEnvelope is the stable response produced by standalone and delegated // evaluators. It is flex-auth's published decision-record contract (§17). type DecisionEnvelope struct { ID string `json:"id" yaml:"id"` ContractVersion string `json:"contract_version,omitempty" yaml:"contract_version,omitempty"` 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"` Binding *DecisionBinding `json:"binding,omitempty" yaml:"binding,omitempty"` Lifetime *DecisionLifetime `json:"lifetime,omitempty" yaml:"lifetime,omitempty"` 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"` } // DecisionLifetimeKind identifies how an allow ends. type DecisionLifetimeKind string const ( DecisionLifetimeTTL DecisionLifetimeKind = "ttl" ) // DefaultAllowTTL is the engine default when a policy package omits allow_ttl. const DefaultAllowTTL = 15 * time.Minute // ReasonAllowLifetimeUnstated is the deny reason for an allow with no stated end. const ReasonAllowLifetimeUnstated = "allow_lifetime_unstated" // DecisionLifetime bounds an allow (§9.7.1). flex-auth has no session concept, // so the first honest shape is a policy-package-declared TTL. type DecisionLifetime struct { Kind DecisionLifetimeKind `json:"kind" yaml:"kind"` TTL string `json:"ttl,omitempty" yaml:"ttl,omitempty"` NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"` ExpiresAt string `json:"expires_at" yaml:"expires_at"` } // DecisionBinding is the exact normalized authorization request evaluated by // a decision. It lets a consumer verify structured action, target, actor, and // context fields without parsing reason or diagnostic prose. type DecisionBinding struct { Tenant string `json:"tenant,omitempty" yaml:"tenant,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"` RequestDigest string `json:"request_digest" yaml:"request_digest"` } // requestDigestMaterial is the exact tuple hashed for §6.4.2 replay. Request // id, policy version, and caring_context are excluded: id is correlation, the // version is provenance, and caring_context is an input-claim digest. type requestDigestMaterial struct { Tenant string `json:"tenant,omitempty"` Subject SubjectRef `json:"subject"` Action string `json:"action"` Resource ResourceRef `json:"resource"` Context map[string]any `json:"context,omitempty"` } // NewDecisionBinding returns a stable structured binding for the exact request // an evaluator consumed. func NewDecisionBinding(request CheckRequest) *DecisionBinding { contextCopy := make(map[string]any, len(request.Context)) for key, value := range request.Context { contextCopy[key] = value } return &DecisionBinding{ Tenant: request.Tenant, Subject: request.Subject, Action: request.Action, Resource: request.Resource, Context: contextCopy, RequestDigest: RequestDigest(request), } } // RequestDigest is the mechanical §6.4.2 replay test: SHA-256 over canonical // JSON of tenant, subject, action, resource, and context. func RequestDigest(request CheckRequest) string { return CanonicalDigest(requestDigestMaterial{ Tenant: request.Tenant, Subject: request.Subject, Action: request.Action, Resource: request.Resource, Context: request.Context, }) } // CanonicalDigest returns "sha256:" plus the hex SHA-256 of canonical JSON. // encoding/json sorts map keys, so two equal Go values agree. func CanonicalDigest(value any) string { data, err := json.Marshal(value) if err != nil { sum := sha256.Sum256(nil) return "sha256:" + hex.EncodeToString(sum[:]) } sum := sha256.Sum256(data) return "sha256:" + hex.EncodeToString(sum[:]) } // InputClaimDigests hashes the request-time claim classes the evaluator joined. func InputClaimDigests(request CheckRequest) map[string]string { digests := make(map[string]string) if len(request.Context) > 0 { digests["context"] = CanonicalDigest(request.Context) } if request.CaringContext != nil { digests["caring_context"] = CanonicalDigest(request.CaringContext) } if len(digests) == 0 { return nil } return digests } // DecisionCompletion carries evaluator-side inputs used to finish an envelope. type DecisionCompletion struct { AllowTTL string Now time.Time } // CompleteDecision stamps contract version, input-claim digests, decision time, // and an explicit allow lifetime. An allow with no stated end becomes a deny. func CompleteDecision(envelope *DecisionEnvelope, request CheckRequest, completion DecisionCompletion) { if envelope == nil { return } if envelope.ContractVersion == "" { envelope.ContractVersion = DecisionRecordContractV1 } if envelope.Provenance.InputClaimDigests == nil { envelope.Provenance.InputClaimDigests = InputClaimDigests(request) } ApplyAllowLifetime(envelope, completion.AllowTTL, completion.Now) } // ParseAllowTTL resolves a package-declared TTL. ok is false when the allow // would have no stated end. Invalid strings return an error so package // validation can reject them. func ParseAllowTTL(declared string) (time.Duration, error) { trimmed := strings.TrimSpace(declared) if trimmed == "" { return DefaultAllowTTL, nil } if strings.EqualFold(trimmed, "none") { return 0, nil } ttl, err := time.ParseDuration(trimmed) if err != nil { return 0, fmt.Errorf("allow_ttl %q is not a Go duration: %w", declared, err) } if ttl <= 0 { return 0, nil } return ttl, nil } // ApplyAllowLifetime sets DecisionTime and, for allows, an explicit TTL. A // missing or zero TTL denies the allow rather than mint a standing grant. func ApplyAllowLifetime(envelope *DecisionEnvelope, declaredTTL string, now time.Time) { if envelope == nil { return } if now.IsZero() { now = time.Now().UTC() } else { now = now.UTC() } if envelope.Provenance.DecisionTime == "" { envelope.Provenance.DecisionTime = now.Format(time.RFC3339) } if envelope.Effect != DecisionEffectAllow { return } ttl, err := ParseAllowTTL(declaredTTL) if err != nil || ttl <= 0 { if envelope.Diagnostics == nil { envelope.Diagnostics = map[string]any{} } if envelope.Reason != "" { envelope.Diagnostics["unstated_allow_reason"] = envelope.Reason } envelope.Effect = DecisionEffectDeny envelope.Reason = ReasonAllowLifetimeUnstated envelope.MatchedRule = ReasonAllowLifetimeUnstated envelope.Lifetime = nil return } display := strings.TrimSpace(declaredTTL) if display == "" { display = "15m" } envelope.Lifetime = &DecisionLifetime{ Kind: DecisionLifetimeTTL, TTL: display, NotBefore: now.Format(time.RFC3339), ExpiresAt: now.Add(ttl).Format(time.RFC3339), } } // ActionAuthorizationStatus is the lifecycle state of a durable authorization. type ActionAuthorizationStatus string const ( ActionAuthorizationPending ActionAuthorizationStatus = "pending" ActionAuthorizationApproved ActionAuthorizationStatus = "approved" ActionAuthorizationDenied ActionAuthorizationStatus = "denied" ActionAuthorizationSuperseded ActionAuthorizationStatus = "superseded" ActionAuthorizationExpired ActionAuthorizationStatus = "expired" ActionAuthorizationRevoked ActionAuthorizationStatus = "revoked" ) // ActionAuthorization joins a durable approval lifecycle to one exact // flex-auth request and decision. Storage and approval collection remain the // responsibility of the organizational decision authority. type ActionAuthorization struct { SchemaVersion string `json:"schema_version" yaml:"schema_version"` ID string `json:"id" yaml:"id"` Status ActionAuthorizationStatus `json:"status" yaml:"status"` SupersededBy string `json:"superseded_by,omitempty" yaml:"superseded_by,omitempty"` Request CheckRequest `json:"request" yaml:"request"` Validity ActionAuthorizationValidity `json:"validity" yaml:"validity"` Approvals ActionAuthorizationApprovals `json:"approvals" yaml:"approvals"` Decision DecisionEnvelope `json:"decision" yaml:"decision"` Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"` } // ActionAuthorizationValidity bounds execution of an approved action. type ActionAuthorizationValidity struct { NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"` ExpiresAt string `json:"expires_at" yaml:"expires_at"` } // ActionAuthorizationApprovals declares the approval threshold and evidence. type ActionAuthorizationApprovals struct { RequiredCount int `json:"required_count" yaml:"required_count"` Entries []ActionAuthorizationApprovalEntry `json:"entries" yaml:"entries"` } // ActionAuthorizationApprovalEntry is one authenticated approver's evidence. type ActionAuthorizationApprovalEntry struct { SubjectID string `json:"subject_id" yaml:"subject_id"` ApprovedAt string `json:"approved_at" yaml:"approved_at"` Assurance string `json:"assurance,omitempty" yaml:"assurance,omitempty"` EvidenceRef string `json:"evidence_ref,omitempty" yaml:"evidence_ref,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"` PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,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"` }