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