mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +00:00
d473398aba
Bundle 1 / Phase 3 (primitive ship): the load-bearing RBAC middleware factory plus its dependencies. Handler conversion sweep (5 admin files: bulk_revocation.go, admin_crl_cache.go, admin_scep_intune.go, admin_est.go, intermediate_ca.go) + m008_admin_gate_test.go registry update is Phase 3.5 follow-on; this commit ships the primitive so 3.5 is mechanical. New context keys (internal/auth/context.go): ActorIDKey, ActorTypeKey, TenantIDKey alongside the legacy UserKey + AdminKey. New helpers GetActorID / GetActorType / GetTenantID with safe fallbacks (UserKey for actor id, ActorTypeAPIKey for missing type, DefaultTenantID for missing tenant). Constants DemoAnonActorID + ActorTypeAPIKey + ActorTypeAnonymous mirror internal/domain/auth without an import cycle. RequirePermission factory (internal/auth/require_permission.go): wraps a handler and gates it behind a named permission. 401 when no actor, 403 when actor lacks permission, 500 on repository error. Skips the gate entirely for protocol endpoints (ACME / SCEP / EST / OCSP / CRL) per the audit's Category F do-not-gate allowlist. PermissionChecker is an interface so internal/auth doesn't depend on internal/service/auth (cmd/server wires the concrete Authorizer at startup). HasPermission is the imperative variant for handlers that branch behaviour rather than 403'ing. ScopeFunc closure extracts the scope type + id from the request for per-resource gating. Protocol-endpoint allowlist (internal/auth/protocol_endpoints.go): IsProtocolEndpoint matches /acme, /scep, /.well-known/est, /.well-known/pki/ocsp, /.well-known/pki/crl prefixes. Adding a new protocol endpoint MUST update this list and add a parallel test. Demo-mode synthetic admin (internal/auth/middleware.go::NewDemoModeAuth): when CERTCTL_AUTH_TYPE=none is configured, this middleware injects ActorID=actor-demo-anon, ActorType=Anonymous, TenantID=t-default, plus the legacy UserKey + AdminKey for back-compat with existing handlers. The synthetic actor's admin-role grant is seeded by migration 000029 so RequirePermission resolves through the JOIN like any other actor. cmd/server startup wires this middleware only when none-mode is configured. API-key middleware extension: NewAuthWithNamedKeys now populates the new keys (ActorIDKey, ActorTypeKey=APIKey, TenantIDKey=t-default) alongside UserKey + AdminKey on every successful Bearer match. Existing handlers continue to read UserKey / IsAdmin until the Phase 3.5 sweep converts them to RequirePermission. Test coverage: TestRequirePermission_NoActorReturns401, TestRequirePermission_GrantedActorReaches200, TestRequirePermission_DeniedActorReturns403, TestRequirePermission_CheckerErrorReturns500, TestRequirePermission_ProtocolEndpointBypassesGate (covers all 5 prefixes), TestRequirePermission_ScopeFnExtractsResourceID, TestIsProtocolEndpoint_PrefixesOnly, TestNewDemoModeAuth_InjectsSyntheticActor, TestNewAuthWithNamedKeys_PopulatesPhase3ContextKeys. fakeChecker pins the contract without a database. Phase 3.5 follow-on (NOT in this commit): convert each of the 5 admin handlers from auth.IsAdmin checks to auth.RequirePermission middleware in router.go; update internal/api/handler/m008_admin_gate_test.go to track auth.RequirePermission call sites instead of (or alongside) auth.IsAdmin; pick the right permission per handler (cert.revoke for bulk_revocation, etc.). Each handler conversion needs the 3-test triplet (_NonAdmin_Returns403 / _AdminExplicitFalse_Returns403 / _AdminPermitted_ForwardsActor) per M-008. Branch: dev/auth-bundle-1. Phase 2 was prior commit (service layer). Phase 3.5 (handler conversion) + Phase 4 (HTTP API) on the next session.
133 lines
5.3 KiB
Go
133 lines
5.3 KiB
Go
// Package auth holds the certctl auth surface: API-key validation, the
|
|
// authenticated-actor context keys, and the helpers that consumers across
|
|
// the codebase use to read the actor identity (rate limiter, audit
|
|
// recorder, handler-level admin gates, GUI affordance hints).
|
|
//
|
|
// Bundle 1 / Phase 0 split this code out of internal/api/middleware so
|
|
// Bundle 2 (OIDC + sessions) and the broader RBAC primitive (roles +
|
|
// permissions + scoped grants) have a clean home that doesn't bloat the
|
|
// generic-middleware package. Phase 0 is a pure refactor; behaviour
|
|
// matches the pre-extract NewAuthWithNamedKeys / NewAuth surface
|
|
// byte-for-byte.
|
|
package auth
|
|
|
|
import "context"
|
|
|
|
// UserKey is the context key for storing the authenticated actor's
|
|
// canonical name. Populated by Middleware (a.k.a. NewAuthWithNamedKeys)
|
|
// from the matched NamedAPIKey.Name. Read by GetUser.
|
|
type UserKey struct{}
|
|
|
|
// AdminKey is the context key for storing the admin flag. Populated by
|
|
// Middleware from the matched NamedAPIKey.Admin. Read by IsAdmin.
|
|
//
|
|
// Bundle 1 keeps the boolean shape for backwards compatibility with the
|
|
// pre-RBAC handler gates. Phase 3 introduces RequirePermission and the
|
|
// boolean becomes informational only (admin role membership ↔ this flag).
|
|
type AdminKey struct{}
|
|
|
|
// GetUser extracts the authenticated user from context. Returns the name
|
|
// of the matched API key, or "" if the request was not authenticated
|
|
// (none mode, missing Bearer, or a misconfigured chain).
|
|
func GetUser(ctx context.Context) string {
|
|
user, ok := ctx.Value(UserKey{}).(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return user
|
|
}
|
|
|
|
// IsAdmin extracts the admin flag from context. Returns true only when
|
|
// the authenticated actor's NamedAPIKey carried Admin=true.
|
|
//
|
|
// Bundle 1 maintains the boolean for back-compat. Bundle 1 Phase 3
|
|
// introduces auth.RequirePermission as the load-bearing authorization
|
|
// gate; legacy IsAdmin callers (5 admin handlers tracked in M-008)
|
|
// migrate to RequirePermission in that phase.
|
|
func IsAdmin(ctx context.Context) bool {
|
|
admin, ok := ctx.Value(AdminKey{}).(bool)
|
|
return ok && admin
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bundle 1 Phase 3: RBAC-aware context keys.
|
|
//
|
|
// ActorIDKey, ActorTypeKey, and TenantIDKey are populated by the auth
|
|
// middleware (NewAuthWithNamedKeys, NewDemoModeAuth, and Bundle 2's
|
|
// session middleware) so that downstream RBAC checks have a stable
|
|
// identity + tenancy view of the caller.
|
|
//
|
|
// UserKey + AdminKey continue to be populated for back-compat with
|
|
// existing audit / rate-limiter / handler code; the new keys are the
|
|
// canonical Phase 3+ identity.
|
|
// =============================================================================
|
|
|
|
// ActorIDKey is the canonical actor identifier (e.g. an API-key name,
|
|
// an OIDC user id, or the synthetic `actor-demo-anon`). Phase 3
|
|
// middleware populates this; auth.RequirePermission and
|
|
// auth.CallerFromContext read it.
|
|
type ActorIDKey struct{}
|
|
|
|
// ActorTypeKey is the typed-string actor type (User, System, Agent,
|
|
// APIKey, Anonymous) corresponding to internal/domain.ActorType. Stored
|
|
// as a string so the internal/auth package doesn't need to import the
|
|
// domain package and create a cycle.
|
|
type ActorTypeKey struct{}
|
|
|
|
// TenantIDKey is the tenant the request executes in. Bundle 1 ships
|
|
// single-tenant; every authenticated request gets the seeded
|
|
// `t-default` tenant unless the future managed-service offering
|
|
// configures a different one.
|
|
type TenantIDKey struct{}
|
|
|
|
// GetActorID returns the canonical actor id from context, or "" when
|
|
// no actor is present (anonymous request, missing middleware in test
|
|
// harnesses, etc.). Falls back to the legacy UserKey value for
|
|
// back-compat with handlers that have not yet adopted the new keys.
|
|
func GetActorID(ctx context.Context) string {
|
|
if id, ok := ctx.Value(ActorIDKey{}).(string); ok && id != "" {
|
|
return id
|
|
}
|
|
return GetUser(ctx)
|
|
}
|
|
|
|
// GetActorType returns the actor type string from context, or "" when
|
|
// no actor type was set. Phase 3 middleware sets this to "APIKey" for
|
|
// validated bearer-token requests and "Anonymous" for the demo-mode
|
|
// synthetic actor.
|
|
func GetActorType(ctx context.Context) string {
|
|
if t, ok := ctx.Value(ActorTypeKey{}).(string); ok {
|
|
return t
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetTenantID returns the tenant id from context, or the seeded
|
|
// default tenant when no value was set. Returning the default rather
|
|
// than "" keeps RBAC lookups working in deployments that haven't
|
|
// configured a tenant explicitly (the Bundle 1 baseline).
|
|
func GetTenantID(ctx context.Context) string {
|
|
if t, ok := ctx.Value(TenantIDKey{}).(string); ok && t != "" {
|
|
return t
|
|
}
|
|
return DefaultTenantID
|
|
}
|
|
|
|
// DefaultTenantID is the seeded single tenant. Mirrors
|
|
// internal/domain/auth.DefaultTenantID; duplicated here to avoid a
|
|
// cross-package import in the hot-path middleware.
|
|
const DefaultTenantID = "t-default"
|
|
|
|
// DemoAnonActorID is the synthetic actor id used by the demo-mode
|
|
// auth middleware when CERTCTL_AUTH_TYPE=none. Mirrors
|
|
// internal/domain/auth.DemoAnonActorID.
|
|
const DemoAnonActorID = "actor-demo-anon"
|
|
|
|
// ActorTypeAPIKey + ActorTypeAnonymous mirror the corresponding
|
|
// domain.ActorType values. Stored as untyped strings here so callers
|
|
// don't have to import the domain package.
|
|
const (
|
|
ActorTypeAPIKey = "APIKey"
|
|
ActorTypeAnonymous = "Anonymous"
|
|
)
|