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.
This commit is contained in:
75
security/identity.go
Normal file
75
security/identity.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package security
|
||||
|
||||
import "context"
|
||||
|
||||
// Identity represents the authenticated principal for a request.
|
||||
//
|
||||
// Identity is a value type — always copied, never a pointer — to prevent
|
||||
// nil-check burden and accidental mutation of a shared context value.
|
||||
// Construction follows a two-step pattern: NewIdentity populates authentication
|
||||
// data from the token (uid, name, email); WithTenant optionally enriches with a
|
||||
// tenant ID in a later middleware step, returning a new value without mutating
|
||||
// the original.
|
||||
type Identity struct {
|
||||
UID string
|
||||
TenantID string
|
||||
DisplayName string
|
||||
Email string
|
||||
}
|
||||
|
||||
// authContextKey is the unexported context key used to store Identity values.
|
||||
// Using a private type prevents collisions with keys from other packages.
|
||||
type authContextKey struct{}
|
||||
|
||||
var authKey = authContextKey{}
|
||||
|
||||
// NewIdentity creates an Identity from token authentication data.
|
||||
// TenantID is left empty — populate it later with WithTenant once the enrichment
|
||||
// middleware has resolved it from the request context.
|
||||
func NewIdentity(uid, displayName, email string) Identity {
|
||||
return Identity{
|
||||
UID: uid,
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTenant returns a copy of the Identity with TenantID set to id.
|
||||
// The receiver is not mutated — safe to call from concurrent middleware.
|
||||
func (i Identity) WithTenant(id string) Identity {
|
||||
i.TenantID = id
|
||||
return i
|
||||
}
|
||||
|
||||
// SetInContext stores id in ctx as a [SecurityBag] and returns the enriched context.
|
||||
// Callers that need to attach additional request-level attributes should use
|
||||
// [SetBagInContext] directly. [FromContext] continues to work unchanged.
|
||||
func SetInContext(ctx context.Context, id Identity) context.Context {
|
||||
return SetBagInContext(ctx, NewSecurityBag(id))
|
||||
}
|
||||
|
||||
// FromContext retrieves the Identity stored by [SetInContext] or [SetBagInContext].
|
||||
// Returns the zero-value Identity and false if no identity is present in ctx.
|
||||
func FromContext(ctx context.Context) (Identity, bool) {
|
||||
bag, ok := BagFromContext(ctx)
|
||||
if !ok {
|
||||
return Identity{}, false
|
||||
}
|
||||
return bag.Identity(), true
|
||||
}
|
||||
|
||||
// SetBagInContext stores bag in ctx and returns the enriched context.
|
||||
// Use this when you need to attach request-level attributes beyond the Identity
|
||||
// (hardware IDs, grant codes, etc.) via [SecurityBag.With].
|
||||
func SetBagInContext(ctx context.Context, bag SecurityBag) context.Context {
|
||||
return context.WithValue(ctx, authKey, bag)
|
||||
}
|
||||
|
||||
// BagFromContext retrieves the [SecurityBag] stored by [SetBagInContext] or [SetInContext].
|
||||
// Returns an empty SecurityBag and false if no bag is present in ctx.
|
||||
// Permission providers use this to access both the Identity and any extra attributes
|
||||
// injected during enrichment.
|
||||
func BagFromContext(ctx context.Context) (SecurityBag, bool) {
|
||||
bag, ok := ctx.Value(authKey).(SecurityBag)
|
||||
return bag, ok
|
||||
}
|
||||
Reference in New Issue
Block a user