flex-auth/pkg/api/canonical.go
tegwick 56940727bf
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 57s
Finish FLEX-WP-0019 layer-model v0.7 conformance
Close the remaining PDP obligations: mechanical layer declaration check,
registry-snapshot digest in provenance, explicit allow TTL, per-input-class
freshness deadlines, and the published decision-record contract. Document
the canonical request digest as the §6.4.2 replay test.

Assistant: grok
Assistant-Session: 01a06256-fb71-7102-b3a9-27e6734257d0
2026-09-03 23:48:45 +02:00

511 lines
27 KiB
Go

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"`
}