Documentation
¶
Overview ¶
Package auth provides authentication and authorization for NornicDB.
This package implements JWT authentication with role-based access control, designed to meet regulatory compliance requirements:
- GDPR Art.32: Technical and organizational measures to ensure security
- HIPAA §164.312(a): Access Control - Unique User Identification
- FISMA AC-2: Account Management
- SOC 2 CC6.1: Logical access controls
Architecture:
- JWT tokens (HS256 algorithm) for stateless authentication
- Multiple credential sources: Bearer header, cookies, query parameters
- Role-based access control (RBAC) with 4 roles: admin, editor, viewer, none
- Account lockout after failed login attempts
- Password hashing with bcrypt
- Audit logging for compliance
Example Usage:
// Initialize authenticator
config := auth.DefaultAuthConfig()
config.JWTSecret = []byte("your-secret-key-min-32-chars")
config.MinPasswordLength = 12
authenticator, err := auth.NewAuthenticator(config)
if err != nil {
log.Fatal(err)
}
// Set audit logging (required for HIPAA/GDPR)
authenticator.SetAuditLogger(func(event auth.AuditEvent) {
log.Printf("[AUDIT] %s: %s (success=%v)",
event.EventType, event.Username, event.Success)
})
// Create users
admin, _ := authenticator.CreateUser("admin", "SecurePass123!",
[]auth.Role{auth.RoleAdmin})
viewer, _ := authenticator.CreateUser("alice", "AlicePass456!",
[]auth.Role{auth.RoleViewer})
// Authenticate and get JWT token
tokenResp, user, err := authenticator.Authenticate(
"admin", "SecurePass123!", "192.168.1.1", "Mozilla/5.0")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Token: %s\n", tokenResp.AccessToken)
fmt.Printf("Type: %s\n", tokenResp.TokenType) // "Bearer"
// Validate token
claims, err := authenticator.ValidateToken(tokenResp.AccessToken)
if err != nil {
log.Fatal(err)
}
// Check permissions
if user.HasPermission(auth.PermWrite) {
fmt.Println("User can write")
}
OAuth 2.0 Compatibility:
The token endpoint follows RFC 6749 (OAuth 2.0) format:
POST /auth/token Content-Type: application/x-www-form-urlencoded grant_type=password&username=alice&password=secret
Response:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600 // omitted if never expires
}
Compliance Features:
GDPR Art.32 (Security):
- Password hashing (bcrypt with configurable cost)
- Token-based authentication (no session storage)
- Account lockout (prevents brute force)
- Audit logging (tracks all authentication events)
HIPAA §164.312(a) (Access Control):
- Unique user identification (User.ID)
- Role-based permissions
- Account disable/enable
- Failed login tracking
FISMA AC-2 (Account Management):
- User creation with roles
- Account lockout
- Audit trail
- Password policy enforcement
Security Best Practices:
- bcrypt for password hashing (adjustable cost)
- HMAC-SHA256 for JWT signatures
- Constant-time string comparison (prevents timing attacks)
- Account lockout after N failed attempts
- No password in logs or responses
ELI12 (Explain Like I'm 12):
Think of this like your school's login system:
1. **Creating an account**: Like signing up for a school portal with username/password 2. **Logging in**: Like entering your credentials at the library computer 3. **JWT token**: Like a hall pass that proves you're allowed to be here 4. **Roles**: Like student vs teacher vs admin - different people can do different things 5. **Account lockout**: If you get your password wrong 5 times, you're locked out for 15 minutes 6. **Audit log**: Like the principal keeping a record of who entered the building and when
The system makes sure only the right people can access the right data!
Plan 04-06-05: auth_attempts_total wiring (D-11 + D-05e + GAP-6 / MET-15).
Design (CONTEXT D-05e + RESEARCH §Q11):
- The pkg/observability.AuthMetrics bag is the single owner of the `nornicdb_auth_attempts_total{result, protocol}` counter family. pkg/auth never imports prometheus directly — it talks to the bag.
- The shared core Authenticator.Authenticate is protocol-agnostic and does NOT increment the counter; the protocol label belongs at the protocol-specific adapter chokepoint (Bolt: handleHello; HTTP: middleware; gRPC: UnaryInterceptor).
- classifyAuthResult is the closed-enum mapper from the Authenticate (..., error) return shape to the result enum.
- RecordAttempt is the SOLE observation entry point — DRY chokepoint used by every protocol adapter. Nil-safe: when the bag is not injected (e.g., test fixtures, embedded usage without a Provider), RecordAttempt is a no-op.
**Audit boundary (T-04-06):** this file ships ZERO changes to pkg/audit/audit.go. The auth_attempts_total counter is a parallel observability signal, not a replacement for the audit log. Operators observe `auth_attempts_total{result="failure"}` rate; compliance auditors read the audit log.
**PII discipline (T-04-01 / ASVS V2):** no `user`, `user_id`, `ip`, or `email` label is exposed. Phase 3 D-03a forbidden-label panic at observability.NewCounterVec catches any attempt to drift.
Package auth: canonical enumeration of RBAC entitlements.
Entitlements are the individual permissions/rights that can be assigned to roles. They include global permissions (read, write, admin, etc.) and per-database entitlements (see database, access database, read, write). Use this list for UI, APIs, and documentation so admins can assign and audit entitlements per role.
Package auth provides OAuth 2.0 authentication support for NornicDB.
This package implements OAuth 2.0 authorization code flow with token validation and refresh capabilities. It integrates with external OAuth providers to authenticate users and issue NornicDB JWT tokens.
OAuth Flow:
- User initiates OAuth login → redirects to OAuth provider
- User authenticates with OAuth provider
- OAuth provider redirects back with authorization code
- Exchange code for access token
- Get user info from OAuth provider
- Create/find NornicDB user account
- Issue NornicDB JWT token
- Validate OAuth tokens on subsequent requests
Security Features:
- CSRF protection via state parameter
- OAuth token validation on each request
- Automatic token refresh when expired
- Short-lived NornicDB tokens (match OAuth token expiry)
Example Usage:
// Configure OAuth
os.Setenv("NORNICDB_AUTH_PROVIDER", "oauth")
os.Setenv("NORNICDB_OAUTH_ISSUER", "http://localhost:8888")
os.Setenv("NORNICDB_OAUTH_CLIENT_ID", "nornicdb-local-test")
os.Setenv("NORNICDB_OAUTH_CLIENT_SECRET", "local-test-secret-123")
os.Setenv("NORNICDB_OAUTH_CALLBACK_URL", "http://localhost:7474/auth/oauth/callback")
// Create OAuth manager
oauthMgr := oauth.NewManager(authenticator)
// Generate authorization URL
state, authURL, err := oauthMgr.GenerateAuthURL("http://localhost:7474")
// Handle callback
user, token, err := oauthMgr.HandleCallback(code, state)
Package auth provides request-scoped RBAC context helpers. When the server mounts authenticated endpoints (Bifrost, GraphQL), it can attach principal roles, DatabaseAccessMode, and ResolvedAccess resolver to the request context so handlers and resolvers can enforce per-database access.
Index ¶
- Constants
- Variables
- func ClassifyAuthResult(err error) string
- func ExtractToken(authHeader, apiKeyHeader, cookie, queryToken, queryAPIKey string) string
- func GlobalEntitlementIDs() []string
- func HasCredentials(authHeader, apiKeyHeader, cookie, queryToken, queryAPIKey string) bool
- func IsBuiltinRole(name string) bool
- func PerDatabaseEntitlementIDs() []string
- func PermissionsForRole(role string, store *RoleEntitlementsStore) []string
- func PermissionsForRoles(roles []string, store *RoleEntitlementsStore) []string
- func RequestPrincipalRolesFromContext(ctx context.Context) []string
- func RequestResolvedAccessResolverFromContext(ctx context.Context) func(string) ResolvedAccess
- func RolePermissionsAsStrings() map[string][]string
- func SecureCompare(a, b string) bool
- func ValidRole(r Role) bool
- func WithRequestDatabaseAccessMode(ctx context.Context, mode DatabaseAccessMode) context.Context
- func WithRequestPrincipalRoles(ctx context.Context, roles []string) context.Context
- func WithRequestResolvedAccessResolver(ctx context.Context, fn func(string) ResolvedAccess) context.Context
- type AllowlistStore
- func (a *AllowlistStore) Allowlist() map[string][]string
- func (a *AllowlistStore) DeleteRoleDatabases(ctx context.Context, role string) error
- func (a *AllowlistStore) HasAllowlistData(ctx context.Context) (bool, error)
- func (a *AllowlistStore) Load(ctx context.Context) error
- func (a *AllowlistStore) RenameRoleInAllowlist(ctx context.Context, oldName, newName string) error
- func (a *AllowlistStore) SaveRoleDatabases(ctx context.Context, role string, databases []string) error
- func (a *AllowlistStore) SeedIfEmpty(ctx context.Context, databaseNames []string) error
- type AuditEvent
- type AuthConfig
- type AuthMetricsRecorder
- type Authenticator
- func (a *Authenticator) AuthMetrics() *observability.AuthMetrics
- func (a *Authenticator) Authenticate(username, password, ipAddress, userAgent string) (*TokenResponse, *User, error)
- func (a *Authenticator) ChangePassword(username, oldPassword, newPassword string) error
- func (a *Authenticator) CreateUser(username, password string, roles []Role) (*User, error)
- func (a *Authenticator) DeleteUser(username string) error
- func (a *Authenticator) DisableUser(username string) error
- func (a *Authenticator) EnableUser(username string) error
- func (a *Authenticator) GenerateAPIToken(user *User, subject string, expiry time.Duration) (string, error)
- func (a *Authenticator) GenerateClusterToken(nodeID string, role Role) (string, error)
- func (a *Authenticator) GenerateClusterTokenWithExpiry(nodeID string, role Role, expiry time.Duration) (string, error)
- func (a *Authenticator) GetUser(username string) (*User, error)
- func (a *Authenticator) GetUserByID(id string) (*User, error)
- func (a *Authenticator) IsSecurityEnabled() bool
- func (a *Authenticator) ListUsers() []*User
- func (a *Authenticator) RecordAttempt(result, protocol string)
- func (a *Authenticator) SetAuditLogger(fn func(AuditEvent))
- func (a *Authenticator) SetAuthMetrics(bag *observability.AuthMetrics)
- func (a *Authenticator) UnlockUser(username string) error
- func (a *Authenticator) UpdateRoles(username string, newRoles []Role) error
- func (a *Authenticator) UpdateUser(username string, email string, metadata map[string]string) error
- func (a *Authenticator) UserCount() int
- func (a *Authenticator) ValidateToken(token string) (*JWTClaims, error)
- type BasicAuthCache
- func (c *BasicAuthCache) Get(username, password string) (*JWTClaims, bool)
- func (c *BasicAuthCache) GetFromHeader(authHeader string) (*JWTClaims, bool)
- func (c *BasicAuthCache) Set(username, password string, claims *JWTClaims)
- func (c *BasicAuthCache) SetFromHeader(authHeader string, claims *JWTClaims)
- type DatabaseAccessMode
- type DbPrivilege
- type Entitlement
- type EntitlementCategory
- type JWTClaims
- type OAuthConfig
- type OAuthManager
- func (m *OAuthManager) ExchangeCode(code string) (*OAuthTokenData, error)
- func (m *OAuthManager) GenerateAuthURL(baseURL string) (string, string, error)
- func (m *OAuthManager) GetUserInfo(accessToken string) (*OAuthUserInfo, error)
- func (m *OAuthManager) HandleCallback(code, state string) (*User, string, time.Time, error)
- func (m *OAuthManager) RefreshOAuthToken(user *User, refreshToken string) error
- func (m *OAuthManager) ValidateOAuthToken(user *User) error
- func (m *OAuthManager) ValidateState(state string) error
- type OAuthTokenData
- type OAuthUserInfo
- type Permission
- type PrivilegeDatabaseRef
- type PrivilegesStore
- func (p *PrivilegesStore) Load(ctx context.Context) error
- func (p *PrivilegesStore) Matrix() []struct{ ... }
- func (p *PrivilegesStore) PutMatrix(ctx context.Context, entries []struct{ ... }) error
- func (p *PrivilegesStore) Resolve(principalRoles []string, dbName string) ResolvedAccess
- func (p *PrivilegesStore) SavePrivilege(ctx context.Context, role, dbName string, read, write bool) error
- type ResolvedAccess
- type Role
- type RoleEntitlementsStore
- type RoleStore
- func (r *RoleStore) AllRoles() []string
- func (r *RoleStore) CreateRole(ctx context.Context, name string) error
- func (r *RoleStore) DeleteRole(ctx context.Context, name string) error
- func (r *RoleStore) Exists(name string) bool
- func (r *RoleStore) Load(ctx context.Context) error
- func (r *RoleStore) RenameRole(ctx context.Context, oldName, newName string) error
- type TokenResponse
- type User
Constants ¶
const ( // DefaultAuthCacheEntries is the default max size for auth caches. DefaultAuthCacheEntries = 1024 // DefaultAuthCacheTTL is the default TTL for cached auth results. DefaultAuthCacheTTL = 30 * time.Second )
Variables ¶
var ( ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") ErrInvalidCredentials = errors.New("invalid credentials") ErrAccountLocked = errors.New("account locked due to failed login attempts") ErrPasswordTooShort = errors.New("password does not meet minimum length requirement") ErrInvalidToken = errors.New("invalid or expired token") ErrInsufficientRole = errors.New("insufficient role permissions") ErrSessionExpired = errors.New("session expired") ErrNoCredentials = errors.New("no credentials provided") ErrMissingSecret = errors.New("JWT secret not configured") )
Errors for authentication operations.
var ( ErrInvalidRoleName = errors.New("invalid role name") ErrRoleExists = errors.New("role already exists") ErrRoleNotFound = errors.New("role not found") ErrCannotDeleteBuiltinRole = errors.New("cannot delete or rename built-in role") )
Role store errors.
var RolePermissions = map[Role][]Permission{ RoleAdmin: {PermRead, PermWrite, PermCreate, PermDelete, PermAdmin, PermSchema, PermUserManage}, RoleEditor: {PermRead, PermWrite, PermCreate, PermDelete}, RoleViewer: {PermRead}, RoleNone: {}, }
RolePermissions maps roles to their allowed permissions. Follows Neo4j's RBAC model.
Functions ¶
func ClassifyAuthResult ¶ added in v1.1.0
ClassifyAuthResult maps the (User, error) return from Authenticate or ValidateToken into the closed result enum {success, failure, denied}.
Mapping per CONTEXT D-05e:
err == nil → "success" errors.Is(err, ErrInvalidCredentials) → "failure" (bad password) errors.Is(err, ErrInvalidToken) → "failure" (bad token) errors.Is(err, ErrSessionExpired) → "failure" (expired) errors.Is(err, ErrAccountLocked) → "denied" (lockout) errors.Is(err, ErrInsufficientRole) → "denied" (RBAC reject) errors.Is(err, ErrNoCredentials) → "denied" (no creds) other (e.g. storage error) → "failure" (defensive)
The mapping is intentional:
- "success" = identity established
- "failure" = credentials presented but rejected
- "denied" = request rejected before / despite credentials (gate)
Operators write per-result-rate alerts:
rate(auth_attempts_total{result="failure"}[5m]) > 1 ⇒ brute force
rate(auth_attempts_total{result="denied"}[5m]) > 0.1 ⇒ misconfig
func ExtractToken ¶
func GlobalEntitlementIDs ¶
func GlobalEntitlementIDs() []string
GlobalEntitlementIDs returns the IDs of all global entitlements (same as Permission values for read/write/create/delete/admin/schema/user_manage).
func HasCredentials ¶
HasCredentials checks if a request has any form of authentication credentials. Checks: Authorization header, X-API-Key header, cookie, query params.
func IsBuiltinRole ¶
IsBuiltin returns true if the role name is a built-in (admin, editor, viewer).
func PerDatabaseEntitlementIDs ¶
func PerDatabaseEntitlementIDs() []string
PerDatabaseEntitlementIDs returns the IDs of per-database entitlements.
func PermissionsForRole ¶
func PermissionsForRole(role string, store *RoleEntitlementsStore) []string
PermissionsForRole returns the effective entitlement IDs (permission strings) for a single role. If store is nil or has no override for role, built-in roles use RolePermissions; user-defined return nil.
func PermissionsForRoles ¶
func PermissionsForRoles(roles []string, store *RoleEntitlementsStore) []string
PermissionsForRoles returns the union of effective entitlement IDs for the given roles. Used by server hasPermission and GetEffectivePermissions.
func RequestPrincipalRolesFromContext ¶
RequestPrincipalRolesFromContext returns the principal's roles from the request context, or nil.
func RequestResolvedAccessResolverFromContext ¶
func RequestResolvedAccessResolverFromContext(ctx context.Context) func(string) ResolvedAccess
RequestResolvedAccessResolverFromContext returns the ResolvedAccess resolver from context, or nil.
func RolePermissionsAsStrings ¶
RolePermissionsAsStrings returns role → permission IDs for fallback when role entitlements store has no override. Used by Bolt and other consumers that need the default role→permissions mapping. IDs match GlobalEntitlementIDs().
func SecureCompare ¶
SecureCompare performs a constant-time string comparison. Prevents timing attacks on token validation.
func WithRequestDatabaseAccessMode ¶
func WithRequestDatabaseAccessMode(ctx context.Context, mode DatabaseAccessMode) context.Context
WithRequestDatabaseAccessMode attaches the principal's per-database access mode to the context.
func WithRequestPrincipalRoles ¶
WithRequestPrincipalRoles attaches the principal's role names to the context.
func WithRequestResolvedAccessResolver ¶
func WithRequestResolvedAccessResolver(ctx context.Context, fn func(string) ResolvedAccess) context.Context
WithRequestResolvedAccessResolver attaches a resolver (dbName -> ResolvedAccess) to the context.
Types ¶
type AllowlistStore ¶
type AllowlistStore struct {
// contains filtered or unexported fields
}
AllowlistStore loads and saves role→databases allowlist in the system database.
func NewAllowlistStore ¶
func NewAllowlistStore(systemStorage storage.Engine) *AllowlistStore
NewAllowlistStore creates a store that reads/writes allowlist to the given system storage.
func (*AllowlistStore) Allowlist ¶
func (a *AllowlistStore) Allowlist() map[string][]string
Allowlist returns a copy of the current allowlist (role → databases). Nil or empty slice means "all databases".
func (*AllowlistStore) DeleteRoleDatabases ¶
func (a *AllowlistStore) DeleteRoleDatabases(ctx context.Context, role string) error
DeleteRoleDatabases removes one role's allowlist entry (node + in-memory). Used when a user-defined role is deleted or renamed.
func (*AllowlistStore) HasAllowlistData ¶
func (a *AllowlistStore) HasAllowlistData(ctx context.Context) (bool, error)
HasAllowlistData returns true if any allowlist entry exists in storage (for seed skip).
func (*AllowlistStore) Load ¶
func (a *AllowlistStore) Load(ctx context.Context) error
Load reads the full allowlist from storage into memory. Call at startup and after PUT.
func (*AllowlistStore) RenameRoleInAllowlist ¶
func (a *AllowlistStore) RenameRoleInAllowlist(ctx context.Context, oldName, newName string) error
RenameRoleInAllowlist copies allowlist entry from oldName to newName and deletes old. Call when a user-defined role is renamed so DB access is preserved.
func (*AllowlistStore) SaveRoleDatabases ¶
func (a *AllowlistStore) SaveRoleDatabases(ctx context.Context, role string, databases []string) error
SaveRoleDatabases persists one role's database list and refreshes in-memory allowlist.
func (*AllowlistStore) SeedIfEmpty ¶
func (a *AllowlistStore) SeedIfEmpty(ctx context.Context, databaseNames []string) error
SeedIfEmpty creates default allowlist entries for admin, editor, viewer if no allowlist data exists. Built-in roles are seeded with an empty list so they have access to all databases (including dynamically created ones). databaseNames is unused but kept for API compatibility; callers may pass the current db list from dbManager for logging or future use. Call after Load() so in-memory state is correct; or call on fresh DB before any Load().
type AuditEvent ¶
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
Username string `json:"username,omitempty"`
UserID string `json:"user_id,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Success bool `json:"success"`
Details string `json:"details,omitempty"`
RequestPath string `json:"request_path,omitempty"`
}
AuditEvent represents an authentication-related event for compliance logging. Required for GDPR Art.30, HIPAA §164.312(b), FISMA AU controls.
type AuthConfig ¶
type AuthConfig struct {
// Password policy
MinPasswordLength int
BcryptCost int
// Token settings
JWTSecret []byte
TokenExpiry time.Duration // 0 = never expire (default)
// Lockout settings
MaxFailedLogins int
LockoutDuration time.Duration
// Default admin username (never gets locked out)
// This ensures the root admin account is always accessible
DefaultAdminUsername string
// Feature flags
SecurityEnabled bool
}
AuthConfig holds authentication configuration.
func DefaultAuthConfig ¶
func DefaultAuthConfig() AuthConfig
DefaultAuthConfig returns default authentication configuration. Returns sensible defaults.
type AuthMetricsRecorder ¶ added in v1.1.0
type AuthMetricsRecorder interface {
// RecordAttempt observes a single auth attempt with the given result
// and protocol. Nil-safe — implementations no-op when the bag is nil.
RecordAttempt(result, protocol string)
}
AuthMetricsRecorder is the seam the protocol adapters use to publish the auth_attempts_total counter. The shared Authenticator owns one (set via SetAuthMetrics); protocol adapters can also own their own bag pointer for direct wiring at the chokepoint.
The interface lives in pkg/auth (not pkg/observability) because the caller domain — the protocol adapters — is the one that knows the protocol label value. pkg/observability only knows about the underlying CounterVec.
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
Authenticator manages users and authentication.
func NewAuthenticator ¶
func NewAuthenticator(config AuthConfig, storage storage.Engine) (*Authenticator, error)
NewAuthenticator creates a new Authenticator with the given configuration.
The authenticator manages user accounts, authentication, and authorization. All operations are thread-safe.
Configuration validation:
- If SecurityEnabled=true, JWTSecret is required (min 32 bytes recommended)
- MinPasswordLength defaults to 8 characters
- BcryptCost defaults to bcrypt.DefaultCost (10)
- MaxFailedLogins defaults to 5
- LockoutDuration defaults to 15 minutes
Example:
// Production configuration
config := auth.AuthConfig{
MinPasswordLength: 12,
BcryptCost: 12, // Higher = more secure but slower
JWTSecret: []byte("your-secret-key-at-least-32-chars-long"),
TokenExpiry: 24 * time.Hour, // Tokens expire after 24h
MaxFailedLogins: 5,
LockoutDuration: 30 * time.Minute,
SecurityEnabled: true,
}
auth, err := auth.NewAuthenticator(config)
if err != nil {
log.Fatal(err)
}
// Development configuration (no security)
devConfig := auth.AuthConfig{
SecurityEnabled: false, // All requests allowed
}
auth = auth.NewAuthenticator(devConfig)
Returns error if SecurityEnabled=true but JWTSecret is empty.
Example 1 - Production HIPAA-Compliant Setup:
config := auth.DefaultAuthConfig()
config.SecurityEnabled = true
config.JWTSecret = []byte(os.Getenv("JWT_SECRET")) // Min 32 bytes
config.MinPasswordLength = 12
config.BcryptCost = 12 // High cost for security
config.MaxFailedAttempts = 5
config.LockoutDuration = 30 * time.Minute
config.TokenExpiry = 4 * time.Hour
authenticator, err := auth.NewAuthenticator(config)
if err != nil {
log.Fatal("Failed to initialize auth:", err)
}
// Set up HIPAA-required audit logging
authenticator.SetAuditLogger(func(event auth.AuditEvent) {
auditLogger.Log(audit.Event{
Type: audit.EventLogin,
UserID: event.UserID,
Username: event.Username,
IPAddress: event.IPAddress,
Success: event.Success,
Metadata: map[string]string{"reason": event.Message},
})
})
// Create admin user with strong password
admin, err := authenticator.CreateUser("admin",
"Str0ng!P@ssw0rd#2024", []auth.Role{auth.RoleAdmin})
Example 2 - Multi-Tenant SaaS with Short-Lived Tokens:
config := auth.DefaultAuthConfig()
config.SecurityEnabled = true
config.JWTSecret = loadSecretFromVault()
config.TokenExpiry = 15 * time.Minute // Short-lived tokens
config.MaxFailedAttempts = 3 // Strict lockout
authenticator, err := auth.NewAuthenticator(config)
if err != nil {
return nil, fmt.Errorf("auth init failed: %w", err)
}
// Create per-tenant users
for _, tenant := range tenants {
user, err := authenticator.CreateUser(
fmt.Sprintf("%s-%s", tenant.ID, username),
generateStrongPassword(),
[]auth.Role{auth.RoleEditor},
)
if err != nil {
log.Printf("Failed to create user for tenant %s: %v", tenant.ID, err)
}
}
Example 3 - Development Mode (No Security):
config := auth.DefaultAuthConfig()
config.SecurityEnabled = false // Bypass all auth checks
authenticator, err := auth.NewAuthenticator(config)
if err != nil {
log.Fatal(err)
}
// In dev mode, any token is accepted
// WARNING: Never use in production!
claims, _ := authenticator.ValidateToken("any-token") // Always succeeds
Example 4 - API Integration with Rate Limiting:
config := auth.DefaultAuthConfig()
config.JWTSecret = []byte("secure-secret-32-chars-minimum!!")
config.MaxFailedAttempts = 5
authenticator, err := auth.NewAuthenticator(config)
if err != nil {
return nil, err
}
// Track failed attempts for rate limiting
authenticator.SetAuditLogger(func(event auth.AuditEvent) {
if event.EventType == "login_failed" {
rateLimiter.RecordFailure(event.IPAddress)
if rateLimiter.IsBlocked(event.IPAddress) {
firewall.BlockIP(event.IPAddress, 1*time.Hour)
}
}
})
ELI12:
Think of NewAuthenticator like hiring a bouncer for a club:
- The bouncer checks IDs (validates JWT tokens)
- They remember troublemakers (account lockout after failed attempts)
- They keep a guest list (user database)
- They write down who comes in (audit logging)
- They have different wristbands for VIP, regular, and just-looking (roles)
SecurityEnabled = true means "bouncer is on duty" SecurityEnabled = false means "anyone can walk in" (development only!)
Real-world Analogy:
JWT tokens are like temporary wristbands: - When you log in, you get a wristband (token) - Show the wristband to access stuff (no need to login again) - Wristband expires after a few hours (token expiry) - If you lose it, get a new one by logging in again
Why JWT Instead of Sessions?
Sessions: "I'll remember you" (server stores state) JWT: "Here's a signed badge, prove yourself each time" (stateless) JWT Benefits: - Works across multiple servers (no shared session storage) - Scales better (no memory for sessions) - Mobile-friendly (just store the token)
Security Features:
- Passwords hashed with bcrypt (can't be reversed)
- Account lockout (prevents password guessing)
- Token expiry (stolen tokens eventually die)
- Audit logging (track who did what)
Compliance:
- GDPR Art.32: Security measures ✓
- HIPAA §164.312(a): Access control ✓
- FISMA AC-2: Account management ✓
- SOC 2 CC6.1: Logical access controls ✓
Performance:
- bcrypt hashing: ~50-200ms (intentionally slow for security)
- Token validation: ~1ms (just signature check)
- Thread-safe for concurrent authentication
Thread Safety:
All methods are thread-safe for concurrent use.
Storage Engine (Required):
Users are always persisted to the provided storage engine.
For testing, use a memory storage engine via dependency injection.
// Production: System database storage
systemStorage := dbManager.GetStorage("system")
auth, err := auth.NewAuthenticator(config, systemStorage)
// Testing: Memory storage engine
memoryStorage := storage.NewMemoryEngine()
auth, err := auth.NewAuthenticator(config, memoryStorage)
func (*Authenticator) AuthMetrics ¶ added in v1.1.0
func (a *Authenticator) AuthMetrics() *observability.AuthMetrics
AuthMetrics returns the currently-injected bag (nil if unset). Used by protocol adapters to obtain the bag for direct wiring.
func (*Authenticator) Authenticate ¶
func (a *Authenticator) Authenticate(username, password, ipAddress, userAgent string) (*TokenResponse, *User, error)
Authenticate verifies user credentials and returns a JWT token.
This implements the OAuth 2.0 password grant flow (RFC 6749 Section 4.3). On successful authentication:
- Password is verified with bcrypt
- Failed login counter is reset
- LastLogin timestamp is updated
- JWT token is generated
- Audit event is logged
Security features:
- Account lockout after MaxFailedLogins attempts
- Disabled accounts cannot authenticate
- Timing attack resistant (doesn't reveal if user exists)
- Failed attempts are logged for security monitoring
Parameters:
- username: User's username
- password: User's password (plain text)
- ipAddress: Client IP for audit logging
- userAgent: Client User-Agent for audit logging
Returns:
- TokenResponse: OAuth 2.0 token response (access_token, token_type, expires_in)
- User: User object (without password)
- Error: ErrInvalidCredentials, ErrAccountLocked, or ErrUserNotFound
Example:
token, user, err := auth.Authenticate(
"alice",
"AlicePassword123!",
"192.168.1.100",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
)
if err != nil {
if errors.Is(err, auth.ErrAccountLocked) {
http.Error(w, "Account locked. Try again in 15 minutes.", 423)
return
}
http.Error(w, "Invalid credentials", 401)
return
}
// Use token in Authorization header
fmt.Printf("Authorization: Bearer %s\n", token.AccessToken)
// Set cookie for browser sessions
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: token.AccessToken,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
Compliance:
- HIPAA §164.312(a)(2)(i): Unique User Identification
- HIPAA §164.312(d): Person or Entity Authentication
- GDPR Art.32: Technical measures to ensure security
- FISMA AC-7: Unsuccessful Login Attempts
func (*Authenticator) ChangePassword ¶
func (a *Authenticator) ChangePassword(username, oldPassword, newPassword string) error
ChangePassword updates a user's password.
func (*Authenticator) CreateUser ¶
func (a *Authenticator) CreateUser(username, password string, roles []Role) (*User, error)
CreateUser creates a new user account with the given credentials and roles.
The password is immediately hashed with bcrypt and never stored in plain text. If no roles are specified, defaults to RoleViewer.
Parameters:
- username: Unique username (must not already exist)
- password: Plain text password (will be hashed)
- roles: User roles, or empty slice for default (viewer)
Returns:
- User object (without password hash)
- ErrUserExists if username already taken
- ErrPasswordTooShort if password doesn't meet minimum length
Example:
// Create admin user
admin, err := auth.CreateUser("admin", "SecurePassword123!",
[]auth.Role{auth.RoleAdmin})
if err != nil {
return err
}
// Create regular user (defaults to viewer)
user, err := auth.CreateUser("alice", "AlicePass456!", nil)
// Create user with multiple roles
editor, err := auth.CreateUser("bob", "BobPass789!",
[]auth.Role{auth.RoleEditor, auth.RoleViewer})
Audit event: Logs "user_create" with success/failure.
Compliance: HIPAA §164.308(a)(3)(ii)(A) - Unique user IDs
func (*Authenticator) DeleteUser ¶
func (a *Authenticator) DeleteUser(username string) error
DeleteUser removes a user.
func (*Authenticator) DisableUser ¶
func (a *Authenticator) DisableUser(username string) error
DisableUser disables a user account.
func (*Authenticator) EnableUser ¶
func (a *Authenticator) EnableUser(username string) error
EnableUser re-enables a disabled user account.
func (*Authenticator) GenerateAPIToken ¶
func (a *Authenticator) GenerateAPIToken(user *User, subject string, expiry time.Duration) (string, error)
GenerateAPIToken creates a stateless API token for the given user. These tokens are for MCP servers and other API integrations. The token is not stored; it's generated on-demand and validated by signature.
Parameters:
- user: The authenticated user requesting the token
- subject: A descriptive label for the token (e.g., "my-mcp-server")
- expiry: Token lifetime (0 = never expires)
Returns the raw JWT token string.
func (*Authenticator) GenerateClusterToken ¶
func (a *Authenticator) GenerateClusterToken(nodeID string, role Role) (string, error)
GenerateClusterToken creates a JWT token for cluster inter-node authentication. This token can be used by cluster nodes to authenticate with each other using the same shared JWT secret.
The token contains:
- Sub: node identifier (e.g., "cluster-node-2")
- Username: same as nodeID for identification
- Roles: specified role (typically "admin" for cluster nodes)
- Iat: issued at timestamp
- Exp: expiration (if TokenExpiry is configured, otherwise never expires)
Usage:
Generate the secret (must be same on all nodes): openssl rand -base64 48
Configure all cluster nodes with the same secret: NORNICDB_JWT_SECRET=<your-generated-secret>
Generate a cluster token (from admin API or code): token, _ := authenticator.GenerateClusterToken("node-2", auth.RoleAdmin)
Use the token to connect from other nodes: driver = GraphDatabase.driver("bolt://node1:7687", basic_auth("", token)) # Empty principal triggers bearer/JWT auth
Example:
// On cluster setup, generate tokens for each node
token, err := authenticator.GenerateClusterToken("cluster-node-west", auth.RoleAdmin)
if err != nil {
log.Fatal(err)
}
// Distribute token securely to node-west
// Node-west uses it to connect:
driver, _ := neo4j.NewDriverWithContext(
"bolt://node-east:7687",
neo4j.BasicAuth("", token, ""), // Empty username = bearer auth
)
Security considerations:
- Tokens are signed with HMAC-SHA256 using the shared JWT secret
- All cluster nodes MUST use the same JWT secret
- Store the secret securely (e.g., HashiCorp Vault, K8s Secrets)
- Rotate secrets periodically by generating new tokens
func (*Authenticator) GenerateClusterTokenWithExpiry ¶
func (a *Authenticator) GenerateClusterTokenWithExpiry(nodeID string, role Role, expiry time.Duration) (string, error)
GenerateClusterTokenWithExpiry creates a JWT token with a custom expiration time. Useful for short-lived tokens during initial cluster setup or testing.
Parameters:
- nodeID: identifier for the cluster node (e.g., "node-2", "replica-west")
- role: role to assign (typically RoleAdmin for cluster nodes)
- expiry: token lifetime (e.g., 24*time.Hour, 0 for never expires)
Example:
// Generate a token that expires in 7 days
token, _ := auth.GenerateClusterTokenWithExpiry("node-2", auth.RoleAdmin, 7*24*time.Hour)
func (*Authenticator) GetUser ¶
func (a *Authenticator) GetUser(username string) (*User, error)
GetUser returns user info by username without sensitive data.
func (*Authenticator) GetUserByID ¶
func (a *Authenticator) GetUserByID(id string) (*User, error)
GetUserByID retrieves a user by their ID.
func (*Authenticator) IsSecurityEnabled ¶
func (a *Authenticator) IsSecurityEnabled() bool
IsSecurityEnabled returns whether security is enabled.
func (*Authenticator) ListUsers ¶
func (a *Authenticator) ListUsers() []*User
ListUsers returns all users without sensitive data.
func (*Authenticator) RecordAttempt ¶ added in v1.1.0
func (a *Authenticator) RecordAttempt(result, protocol string)
RecordAttempt implements AuthMetricsRecorder on the shared Authenticator. Protocol adapters that share a single Authenticator instance can call `auth.RecordAttempt(...)` directly with the appropriate protocol label.
Closed-enum discipline at the call site: callers must pass values from observability.AllowedAuthResults × observability.AllowedAuthProtocols. Any drift is caught by Phase 3 D-03a forbidden-label panic at registration (defense in depth — the call site gate is the actual check).
func (*Authenticator) SetAuditLogger ¶
func (a *Authenticator) SetAuditLogger(fn func(AuditEvent))
SetAuditLogger sets the audit logging callback.
func (*Authenticator) SetAuthMetrics ¶ added in v1.1.0
func (a *Authenticator) SetAuthMetrics(bag *observability.AuthMetrics)
SetAuthMetrics injects the observability AuthMetrics bag for the auth_attempts_total counter (D-11 / D-05e / MET-15).
Idempotent. Calling with nil disables observation.
IMPORTANT: this is a parallel observability signal — the audit log (pkg/audit/audit.go) is the compliance source of truth and remains unchanged.
func (*Authenticator) UnlockUser ¶
func (a *Authenticator) UnlockUser(username string) error
UnlockUser manually unlocks a locked user account.
func (*Authenticator) UpdateRoles ¶
func (a *Authenticator) UpdateRoles(username string, newRoles []Role) error
UpdateRoles changes a user's roles.
func (*Authenticator) UpdateUser ¶
UpdateUser updates user profile information (email, metadata). Users can update their own profile, admins can update any user.
func (*Authenticator) UserCount ¶
func (a *Authenticator) UserCount() int
UserCount returns the number of registered users.
func (*Authenticator) ValidateToken ¶
func (a *Authenticator) ValidateToken(token string) (*JWTClaims, error)
ValidateToken validates a JWT token and returns the claims.
The token is verified using HMAC-SHA256. If valid, returns the decoded claims including user ID, username, roles, and expiration.
Validation checks:
- Token format (header.payload.signature)
- Signature verification (HMAC-SHA256)
- Expiration (if Exp > 0)
- Not before (if configured)
If SecurityEnabled=false, returns dummy claims allowing all access.
The token can include "Bearer " prefix (will be stripped).
Parameters:
- token: JWT token string (with or without "Bearer " prefix)
Returns:
- JWTClaims: Decoded claims with user info and roles
- Error: ErrInvalidToken, ErrSessionExpired, or ErrNoCredentials
Example:
// From Authorization header
authHeader := r.Header.Get("Authorization") // "Bearer eyJhbGc..."
claims, err := auth.ValidateToken(authHeader)
if err != nil {
http.Error(w, "Unauthorized", 401)
return
}
fmt.Printf("User: %s\n", claims.Username)
fmt.Printf("Roles: %v\n", claims.Roles)
// Check if user has admin role
hasAdmin := false
for _, role := range claims.Roles {
if role == string(auth.RoleAdmin) {
hasAdmin = true
break
}
}
Security:
- Uses constant-time comparison to prevent timing attacks
- Validates expiration to prevent replay attacks
- Checks signature to prevent tampering
type BasicAuthCache ¶
type BasicAuthCache struct {
// contains filtered or unexported fields
}
BasicAuthCache caches successful Basic authentication results to avoid repeated bcrypt work on high-volume request paths.
func NewBasicAuthCache ¶
func NewBasicAuthCache(maxEntries int, ttl time.Duration) *BasicAuthCache
NewBasicAuthCache creates a new cache for Basic auth results.
func (*BasicAuthCache) Get ¶
func (c *BasicAuthCache) Get(username, password string) (*JWTClaims, bool)
Get retrieves cached claims using username/password (when header isn't available).
func (*BasicAuthCache) GetFromHeader ¶
func (c *BasicAuthCache) GetFromHeader(authHeader string) (*JWTClaims, bool)
GetFromHeader retrieves cached claims using the full Basic auth header.
func (*BasicAuthCache) Set ¶
func (c *BasicAuthCache) Set(username, password string, claims *JWTClaims)
Set caches claims using username/password (when header isn't available).
func (*BasicAuthCache) SetFromHeader ¶
func (c *BasicAuthCache) SetFromHeader(authHeader string, claims *JWTClaims)
SetFromHeader caches claims using the full Basic auth header.
type DatabaseAccessMode ¶
type DatabaseAccessMode interface {
CanSeeDatabase(dbName string) bool
CanAccessDatabase(dbName string) bool
}
DatabaseAccessMode answers whether a principal may see and access databases. Equivalent to Neo4j DatabaseAccessMode. Used before executing Cypher: CanAccessDatabase(dbName) must be true or the request is denied with 403.
var DenyAllDatabaseAccessMode DatabaseAccessMode = denyAllDBAccessMode{}
DenyAllDatabaseAccessMode is the singleton that denies all databases. Use when auth is enabled and no allowlist is configured (secure default).
var FullDatabaseAccessMode DatabaseAccessMode = fullDBAccessMode{}
FullDatabaseAccessMode is the singleton that allows all databases. Use when auth is disabled so dev/local usage works without RBAC.
func NewAllowlistDatabaseAccessMode ¶
func NewAllowlistDatabaseAccessMode(allowlist map[string][]string, principalRoles []string) DatabaseAccessMode
NewAllowlistDatabaseAccessMode returns a DatabaseAccessMode for the given allowlist and principal roles. For each role: if the role has no allowlist (or empty), treat as "all databases"; else allow only listed DBs. CanAccessDatabase(dbName) is true iff at least one of the principal's roles allows the database.
func RequestDatabaseAccessModeFromContext ¶
func RequestDatabaseAccessModeFromContext(ctx context.Context) DatabaseAccessMode
RequestDatabaseAccessModeFromContext returns the principal's DatabaseAccessMode from context, or nil.
type DbPrivilege ¶
DbPrivilege holds read/write for one (role, database).
type Entitlement ¶
type Entitlement struct {
ID string `json:"id"` // Stable ID (e.g. "read", "database_write")
Name string `json:"name"` // Display name
Description string `json:"description"` // What this entitlement gates
Category EntitlementCategory `json:"category"` // global or per_database
}
Entitlement describes a single assignable permission or right.
func AllEntitlements ¶
func AllEntitlements() []Entitlement
AllEntitlements returns the full list of entitlements with descriptions. Use this for UI (assign entitlements to roles), APIs (GET /auth/entitlements), and documentation. Global entitlements map to Permission; per-database entitlements map to allowlist (see/access) and privileges matrix (read/write).
type EntitlementCategory ¶
type EntitlementCategory string
EntitlementCategory indicates whether an entitlement is global or per-database.
const ( EntitlementCategoryGlobal EntitlementCategory = "global" EntitlementCategoryPerDatabase EntitlementCategory = "per_database" )
type JWTClaims ¶
type JWTClaims struct {
Sub string `json:"sub"` // Subject (user ID)
Email string `json:"email,omitempty"` // User email
Username string `json:"username,omitempty"` // Username
Roles []string `json:"roles"` // User roles
Iat int64 `json:"iat"` // Issued at (Unix timestamp)
Exp int64 `json:"exp,omitempty"` // Expiration (Unix timestamp, 0 = never)
}
JWTClaims represents the claims in a JWT token. Standard JWT claims structure.
type OAuthConfig ¶
type OAuthConfig struct {
Provider string // "oauth" to enable
Issuer string // OAuth provider base URL
ClientID string // OAuth client ID
ClientSecret string // OAuth client secret
CallbackURL string // OAuth callback URL
}
OAuthConfig holds OAuth provider configuration.
func GetOAuthConfig ¶
func GetOAuthConfig() *OAuthConfig
GetConfig returns the current OAuth configuration from environment variables.
func (*OAuthConfig) IsConfigured ¶
func (c *OAuthConfig) IsConfigured() bool
IsConfigured checks if OAuth is properly configured.
type OAuthManager ¶
type OAuthManager struct {
// contains filtered or unexported fields
}
OAuthManager manages OAuth 2.0 authentication flow and token validation.
func NewOAuthManager ¶
func NewOAuthManager(authenticator *Authenticator) *OAuthManager
NewOAuthManager creates a new OAuth manager.
func (*OAuthManager) ExchangeCode ¶
func (m *OAuthManager) ExchangeCode(code string) (*OAuthTokenData, error)
ExchangeCode exchanges an authorization code for an access token.
func (*OAuthManager) GenerateAuthURL ¶
func (m *OAuthManager) GenerateAuthURL(baseURL string) (string, string, error)
GenerateAuthURL generates an OAuth authorization URL and stores state for CSRF protection. Returns the state parameter and the authorization URL.
func (*OAuthManager) GetUserInfo ¶
func (m *OAuthManager) GetUserInfo(accessToken string) (*OAuthUserInfo, error)
GetUserInfo retrieves user information from the OAuth provider.
func (*OAuthManager) HandleCallback ¶
HandleCallback handles the OAuth callback, exchanges code for token, gets user info, and creates/updates the NornicDB user account. Returns the user, NornicDB JWT token, and OAuth token expiry time.
func (*OAuthManager) RefreshOAuthToken ¶
func (m *OAuthManager) RefreshOAuthToken(user *User, refreshToken string) error
RefreshOAuthToken attempts to refresh an expired OAuth access token using a refresh token.
func (*OAuthManager) ValidateOAuthToken ¶
func (m *OAuthManager) ValidateOAuthToken(user *User) error
ValidateOAuthToken validates that an OAuth user's OAuth access token is still valid by calling the OAuth provider's userinfo endpoint.
func (*OAuthManager) ValidateState ¶
func (m *OAuthManager) ValidateState(state string) error
ValidateState validates an OAuth state parameter and removes it (one-time use).
type OAuthTokenData ¶
type OAuthTokenData struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token,omitempty"`
}
OAuthTokenData represents an OAuth access token response.
type OAuthUserInfo ¶
type OAuthUserInfo struct {
Sub string `json:"sub"`
Email string `json:"email"`
PreferredUsername string `json:"preferred_username"`
Roles []string `json:"roles"`
}
OAuthUserInfo represents user information from OAuth provider.
type Permission ¶
type Permission string
Permission represents an action that can be performed.
const ( PermRead Permission = "read" PermWrite Permission = "write" PermCreate Permission = "create" PermDelete Permission = "delete" PermAdmin Permission = "admin" PermSchema Permission = "schema" PermUserManage Permission = "user_manage" )
Permissions map to Neo4j-compatible actions.
type PrivilegeDatabaseRef ¶
type PrivilegeDatabaseRef struct {
Name string // Normalized database name, e.g. "nornic", "system"
OwningDatabaseName string // For composite/sharded; empty or Name for normal DBs
}
PrivilegeDatabaseRef identifies the database for privilege checks. Equivalent to Neo4j PrivilegeDatabaseReference (name, owningDatabaseName). For normal (non-composite) databases, Name and OwningDatabaseName are the same.
type PrivilegesStore ¶
type PrivilegesStore struct {
// contains filtered or unexported fields
}
PrivilegesStore persists and resolves per (role, database) read/write.
func NewPrivilegesStore ¶
func NewPrivilegesStore(systemStorage storage.Engine) *PrivilegesStore
NewPrivilegesStore creates a store that reads/writes _DbPrivilege nodes.
func (*PrivilegesStore) Load ¶
func (p *PrivilegesStore) Load(ctx context.Context) error
Load reads all _DbPrivilege nodes from storage into memory.
func (*PrivilegesStore) Matrix ¶
func (p *PrivilegesStore) Matrix() []struct { Role string `json:"role"` Database string `json:"database"` Read bool `json:"read"` Write bool `json:"write"` }
Matrix returns a copy of the full matrix for GET API.
func (*PrivilegesStore) PutMatrix ¶
func (p *PrivilegesStore) PutMatrix(ctx context.Context, entries []struct { Role string `json:"role"` Database string `json:"database"` Read bool `json:"read"` Write bool `json:"write"` }) error
PutMatrix replaces the stored matrix with the given list (for PUT /auth/access/privileges). Deletes all existing _DbPrivilege nodes then creates nodes for each entry.
func (*PrivilegesStore) Resolve ¶
func (p *PrivilegesStore) Resolve(principalRoles []string, dbName string) ResolvedAccess
Resolve returns ResolvedAccess for (principalRoles, dbName). If any role has an entry for this db, aggregate read/write. If none have an entry, fall back to global RolePermissions.
func (*PrivilegesStore) SavePrivilege ¶
func (p *PrivilegesStore) SavePrivilege(ctx context.Context, role, dbName string, read, write bool) error
SavePrivilege persists one (role, database, read, write) and refreshes in-memory.
type ResolvedAccess ¶
type ResolvedAccess struct {
Read bool // Allow MATCH, read properties, etc.
Write bool // Allow CREATE, DELETE, SET, MERGE, etc.
}
ResolvedAccess is the resolved read/write capability for a (principal, database). Equivalent to Neo4j AccessMode for a single DB. Used for execution-time mutation checks: Write must be true to allow CREATE, DELETE, SET, MERGE, etc.
type Role ¶
type Role string
Role represents a user role with associated permissions.
const ( RoleAdmin Role = "admin" // Full access including user management RoleEditor Role = "editor" // Read/write data RoleViewer Role = "viewer" // Read only (default) RoleNone Role = "none" // No access )
Predefined roles following Neo4j conventions.
func ConvertOAuthRoles ¶
ConvertOAuthRoles converts OAuth role strings to auth.Role enum.
func RoleFromString ¶
RoleFromString converts a string to a Role.
type RoleEntitlementsStore ¶
type RoleEntitlementsStore struct {
// contains filtered or unexported fields
}
RoleEntitlementsStore loads and saves role→entitlement IDs (global permissions) in the system database.
func NewRoleEntitlementsStore ¶
func NewRoleEntitlementsStore(systemStorage storage.Engine) *RoleEntitlementsStore
NewRoleEntitlementsStore creates a store that reads/writes role entitlements to the given system storage.
func (*RoleEntitlementsStore) All ¶
func (r *RoleEntitlementsStore) All() map[string][]string
All returns role→entitlement IDs for all roles that have a stored override. For full role→entitlements (including built-in defaults) the server merges with RolePermissions.
func (*RoleEntitlementsStore) Get ¶
func (r *RoleEntitlementsStore) Get(role string) []string
Get returns the stored entitlement IDs for a role. Returns nil if no override is stored. For permission resolution use PermissionsForRole so built-in roles use RolePermissions when not overridden.
type RoleStore ¶
type RoleStore struct {
// contains filtered or unexported fields
}
RoleStore persists user-defined role names in the system database.
func NewRoleStore ¶
NewRoleStore creates a store that reads/writes _Role nodes in the given system storage.
func (*RoleStore) CreateRole ¶
CreateRole creates a user-defined role. Fails if name is built-in or already exists.
func (*RoleStore) DeleteRole ¶
DeleteRole removes a user-defined role. Fails if name is built-in or does not exist. Caller must ensure no user has this role (e.g. check auth.ListUsers()).
func (*RoleStore) RenameRole ¶
RenameRole renames a user-defined role (oldName -> newName). Fails if old is built-in, new is built-in or exists, or old does not exist. Does not update user nodes or allowlist; caller should update those or document that rename is for the role list only and allowlist/user assignments use the new name going forward.
type TokenResponse ¶
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"` // Always "Bearer"
ExpiresIn int64 `json:"expires_in,omitempty"` // Seconds until expiration (omitted if never expires)
Scope string `json:"scope,omitempty"`
}
TokenResponse follows OAuth 2.0 RFC 6749 token response format. Compatible with standard OAuth 2.0 token endpoints.
type User ¶
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
PasswordHash string `json:"-"` // Never serialize
Roles []Role `json:"roles"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login,omitempty"`
FailedLogins int `json:"-"` // Internal tracking
LockedUntil time.Time `json:"-"` // Internal tracking
Disabled bool `json:"disabled,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
User represents an authenticated user account.
Users have:
- Unique ID and username
- Password (hashed with bcrypt, never exposed)
- One or more roles (admin, editor, viewer, none)
- Timestamps for auditing
- Metadata for custom properties
Security features:
- PasswordHash is never serialized (json:"-" tag)
- FailedLogins and LockedUntil track brute force attempts
- Disabled flag allows account suspension
Example:
user := &auth.User{
ID: "usr-abc123",
Username: "alice",
Email: "alice@example.com",
Roles: []auth.Role{auth.RoleEditor},
Metadata: map[string]string{
"department": "Engineering",
"team": "Backend",
},
}
// Check permissions
if user.HasRole(auth.RoleAdmin) {
fmt.Println("User is admin")
}
if user.HasPermission(auth.PermWrite) {
fmt.Println("User can write data")
}
func (*User) HasPermission ¶
func (u *User) HasPermission(perm Permission) bool
HasPermission checks if the user has a specific permission through any of their roles.
Permissions are granted by roles. This method checks all the user's roles and returns true if any role grants the requested permission.
Permission hierarchy:
- RoleAdmin: read, write, create, delete, admin, schema, user_manage
- RoleEditor: read, write, create, delete
- RoleViewer: read
- RoleNone: (no permissions)
Example:
user := &auth.User{
Roles: []auth.Role{auth.RoleEditor},
}
if user.HasPermission(auth.PermRead) {
fmt.Println("Can read") // Printed
}
if user.HasPermission(auth.PermWrite) {
fmt.Println("Can write") // Printed
}
if user.HasPermission(auth.PermUserManage) {
fmt.Println("Can manage users") // NOT printed (needs RoleAdmin)
}
func (*User) HasRole ¶
HasRole checks if the user has a specific role.
A user can have multiple roles. This method returns true if any of the user's roles match the specified role.
Example:
user := &auth.User{
Roles: []auth.Role{auth.RoleEditor, auth.RoleViewer},
}
if user.HasRole(auth.RoleAdmin) {
fmt.Println("Is admin") // Not printed
}
if user.HasRole(auth.RoleEditor) {
fmt.Println("Is editor") // Printed
}