Files
contracts/security/security_bag.go

56 lines
1.9 KiB
Go
Raw Normal View History

feat(contracts): initial implementation (v1.0.0) Introduces code.nochebuena.dev/einherjar/contracts — the zero-dependency foundation of the Einherjar framework. Defines the interfaces and minimal types consumed by every starter. Zero external dependencies. Zero Einherjar dependencies. Nothing is above it in the dependency graph. lifecycle: - Component — OnInit, OnStart, OnStop three-phase lifecycle hooks observability: - Level (LevelCritical=0, LevelDegraded); zero value is the safe default - Checkable — HealthCheck, Name, Priority - Identifiable — ModulePath, ModuleVersion; implemented by all starters to surface module identity and version in the startup banner logging: - Logger — Debug, Info, Warn, Error, With, WithContext errs: - CodedError — ErrorCode() string; satisfied by core/xerrors.Err - ContextualError — ErrorContext() map[string]any; satisfied by core/xerrors.Err security: - Identity value type — UID, TenantID, DisplayName, Email; NewIdentity, WithTenant - Permission (int64), MaxPermission=62, PermissionMask — Has, Grant - PermissionProvider — ResolveMask(ctx, uid, resource) (PermissionMask, error) - SecurityBag value type — immutable request-scoped security context; carries Identity and arbitrary typed attributes (hardware IDs, grant codes, etc.); With copies the attribute map on every call to preserve receiver-invariant behaviour - NewSecurityBag, Identity, WithIdentity, Get, With - SetBagInContext / BagFromContext — full bag context storage - SetInContext / FromContext — backed by SecurityBag; all four cross-function combinations (SetInContext+BagFromContext, SetBagInContext+FromContext) are valid One file per type; CT-6 enforced by compliance test AST walk.
2026-05-29 15:43:08 +00:00
package security
// SecurityBag is the request-scoped security context for a single request.
// It carries the authenticated [Identity] alongside any additional attributes
// injected during the enrichment phase (hardware IDs, grant codes, etc.).
//
// SecurityBag is a value type — all mutation methods return a new value without
// modifying the receiver. The attribute map is copied on every [SecurityBag.With]
// call to preserve this guarantee.
//
// The framework defines no string key constants for bag attributes — all
// framework-known fields live as typed fields on [Identity]. Callers define
// their own constants to avoid collisions:
//
// const KeyHardwareID = "hardware_id"
//
// bag.With(KeyHardwareID, hwID)
// val, ok := bag.Get(KeyHardwareID)
type SecurityBag struct {
identity Identity
attributes map[string]any
}
// NewSecurityBag creates a SecurityBag wrapping id with an empty attribute map.
func NewSecurityBag(id Identity) SecurityBag {
return SecurityBag{identity: id}
}
// Identity returns the authenticated Identity stored in the bag.
func (b SecurityBag) Identity() Identity {
return b.identity
}
// WithIdentity returns a copy of the bag with the Identity replaced by id.
// Used by bag enrichers that modify the Identity (e.g., [authmw.WithTenantHeader]).
func (b SecurityBag) WithIdentity(id Identity) SecurityBag {
return SecurityBag{identity: id, attributes: b.attributes}
}
// Get returns the attribute stored under key and true, or (nil, false) if absent.
func (b SecurityBag) Get(key string) (any, bool) {
v, ok := b.attributes[key]
return v, ok
}
// With returns a copy of the bag with key set to value.
// The receiver is not modified — safe to call from concurrent middleware chains.
func (b SecurityBag) With(key string, value any) SecurityBag {
attrs := make(map[string]any, len(b.attributes)+1)
for k, v := range b.attributes {
attrs[k] = v
}
attrs[key] = value
return SecurityBag{identity: b.identity, attributes: attrs}
}