c.Entitled("action", quantity) checks permission before execution.
Default: everything permitted (trusted conclave).
Consumer packages replace checker via c.SetEntitlementChecker().
- Entitlement struct: Allowed, Unlimited, Limit, Used, Remaining, Reason
- NearLimit(threshold), UsagePercent() convenience methods
- EntitlementChecker function type — registered by go-entitlements/commerce-matrix
- UsageRecorder for consumption tracking after gated actions succeed
- Enforcement wired into Action.Run() — one gate for all capabilities
- Security audit logging on denials (P11-6)
- 16 AX-7 tests including full SaaS gating pattern simulation
Maps 1:1 to RFC-004 EntitlementResult and RFC-005 PermissionResult.
Co-Authored-By: Virgil <virgil@lethean.io>
130 lines
4 KiB
Go
130 lines
4 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// Permission primitive for the Core framework.
|
|
// Entitlement answers "can [subject] do [action] with [quantity]?"
|
|
// Default: everything permitted (trusted conclave).
|
|
// With go-entitlements: checks workspace packages, features, usage, boosts.
|
|
// With commerce-matrix: checks entity hierarchy, lock cascade.
|
|
//
|
|
// Usage:
|
|
//
|
|
// e := c.Entitled("process.run") // boolean gate
|
|
// e := c.Entitled("social.accounts", 3) // quantity check
|
|
// if e.Allowed { proceed() }
|
|
// if e.NearLimit(0.8) { showUpgradePrompt() }
|
|
//
|
|
// Registration:
|
|
//
|
|
// c.SetEntitlementChecker(myChecker)
|
|
// c.SetUsageRecorder(myRecorder)
|
|
package core
|
|
|
|
import "context"
|
|
|
|
// Entitlement is the result of a permission check.
|
|
// Carries context for both boolean gates (Allowed) and usage limits (Limit/Used/Remaining).
|
|
//
|
|
// e := c.Entitled("social.accounts", 3)
|
|
// e.Allowed // true
|
|
// e.Limit // 5
|
|
// e.Used // 2
|
|
// e.Remaining // 3
|
|
// e.NearLimit(0.8) // false
|
|
type Entitlement struct {
|
|
Allowed bool // permission granted
|
|
Unlimited bool // no cap (agency tier, admin, trusted conclave)
|
|
Limit int // total allowed (0 = boolean gate)
|
|
Used int // current consumption
|
|
Remaining int // Limit - Used
|
|
Reason string // denial reason — for UI and audit logging
|
|
}
|
|
|
|
// NearLimit returns true if usage exceeds the threshold percentage.
|
|
//
|
|
// if e.NearLimit(0.8) { showUpgradePrompt() }
|
|
func (e Entitlement) NearLimit(threshold float64) bool {
|
|
if e.Unlimited || e.Limit == 0 {
|
|
return false
|
|
}
|
|
return float64(e.Used)/float64(e.Limit) >= threshold
|
|
}
|
|
|
|
// UsagePercent returns current usage as a percentage of the limit.
|
|
//
|
|
// pct := e.UsagePercent() // 75.0
|
|
func (e Entitlement) UsagePercent() float64 {
|
|
if e.Limit == 0 {
|
|
return 0
|
|
}
|
|
return float64(e.Used) / float64(e.Limit) * 100
|
|
}
|
|
|
|
// EntitlementChecker answers "can [subject] do [action] with [quantity]?"
|
|
// Subject comes from context (workspace, entity, user — consumer's concern).
|
|
type EntitlementChecker func(action string, quantity int, ctx context.Context) Entitlement
|
|
|
|
// UsageRecorder records consumption after a gated action succeeds.
|
|
// Consumer packages provide the implementation (database, cache, etc).
|
|
type UsageRecorder func(action string, quantity int, ctx context.Context)
|
|
|
|
// defaultChecker — trusted conclave, everything permitted.
|
|
func defaultChecker(_ string, _ int, _ context.Context) Entitlement {
|
|
return Entitlement{Allowed: true, Unlimited: true}
|
|
}
|
|
|
|
// Entitled checks if an action is permitted in the current context.
|
|
// Default: always returns Allowed=true, Unlimited=true.
|
|
// Denials are logged via core.Security().
|
|
//
|
|
// e := c.Entitled("process.run")
|
|
// e := c.Entitled("social.accounts", 3)
|
|
func (c *Core) Entitled(action string, quantity ...int) Entitlement {
|
|
qty := 1
|
|
if len(quantity) > 0 {
|
|
qty = quantity[0]
|
|
}
|
|
|
|
e := c.entitlementChecker(action, qty, c.Context())
|
|
|
|
if !e.Allowed {
|
|
Security("entitlement.denied", "action", action, "quantity", qty, "reason", e.Reason)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
// SetEntitlementChecker replaces the default (permissive) checker.
|
|
// Called by go-entitlements or commerce-matrix during OnStartup.
|
|
//
|
|
// func (s *EntitlementService) OnStartup(ctx context.Context) core.Result {
|
|
// s.Core().SetEntitlementChecker(s.check)
|
|
// return core.Result{OK: true}
|
|
// }
|
|
func (c *Core) SetEntitlementChecker(checker EntitlementChecker) {
|
|
c.entitlementChecker = checker
|
|
}
|
|
|
|
// RecordUsage records consumption after a gated action succeeds.
|
|
// Delegates to the registered UsageRecorder. No-op if none registered.
|
|
//
|
|
// e := c.Entitled("ai.credits", 10)
|
|
// if e.Allowed {
|
|
// doWork()
|
|
// c.RecordUsage("ai.credits", 10)
|
|
// }
|
|
func (c *Core) RecordUsage(action string, quantity ...int) {
|
|
if c.usageRecorder == nil {
|
|
return
|
|
}
|
|
qty := 1
|
|
if len(quantity) > 0 {
|
|
qty = quantity[0]
|
|
}
|
|
c.usageRecorder(action, qty, c.Context())
|
|
}
|
|
|
|
// SetUsageRecorder registers a usage tracking function.
|
|
// Called by go-entitlements during OnStartup.
|
|
func (c *Core) SetUsageRecorder(recorder UsageRecorder) {
|
|
c.usageRecorder = recorder
|
|
}
|