guard
API
guard
packageAPI reference for the guard
package.
Imports
(6)
S
struct
CompiledPolicy
CompiledPolicy represents the evaluated security rules for a specific type.
pkg/guard/policy.go:22-31
type CompiledPolicy struct
Methods
Evaluate
Method
Evaluate checks if the user has permission for the action on the resource value.
Parameters
Returns
error
func (*CompiledPolicy) Evaluate(user Identity, resourceVal reflect.Value, action string) error
{
// 1. Check Static Rules
// We need to check exact action match AND wildcard "*" action match.
allowed := false
ruleFound := false
// Helper to check map
checkStatic := func(act string) {
if allowedRoles, ok := p.StaticRules[act]; ok {
ruleFound = true
// Check if user has any of these roles
userRoles := user.GetRoles()
for _, ur := range userRoles {
if allowedRoles[ur] {
allowed = true
return
}
// Check if allowedRoles has "*" (wildcard role allowed?)
if allowedRoles["*"] {
allowed = true
return
}
}
}
}
checkStatic(action)
if allowed {
return nil
}
checkStatic("*")
if allowed {
return nil
}
// 2. Check Dynamic Rules
for _, rule := range p.DynamicRules {
// Check if rule applies to this action
actionMatch := false
for _, a := range rule.Actions {
if a == action || a == "*" {
actionMatch = true
break
}
}
if actionMatch {
ruleFound = true
// Evaluate dynamic role from field
fieldVal := resourceVal.Field(rule.FieldIndex)
// Logic from original:
// if fieldVal is Map, iterate keys/values...
// "role:*" logic:
// "If role tag contains '*', the field value(s) are treated as required roles."
// So we extract roles from fieldVal and check if user has them.
// Reusing checker logic or similar?
// Original:
// if fieldVal.Kind() == reflect.Map ...
// Simplify: extract roles from fieldVal.
dynamicRoles := extractRoles(fieldVal, user.GetID())
userRoles := user.GetRoles()
// Check intersection
for _, dr := range dynamicRoles {
for _, ur := range userRoles {
if dr == ur {
allowed = true
break
}
}
if allowed {
break
}
}
}
if allowed {
break
}
}
if !ruleFound {
return fmt.Errorf("no policy defined for action '%s'", action)
}
if !allowed {
return fmt.Errorf("permission denied for action '%s'", action)
}
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| StaticRules | map[string]map[string]bool | |
| DynamicRules | []DynamicRule |
S
struct
DynamicRule
DynamicRule represents a rule dependent on a field’s value.
pkg/guard/policy.go:34-40
type DynamicRule struct
Fields
| Name | Type | Description |
|---|---|---|
| FieldIndex | int | |
| Actions | []string | |
| IsWildcard | bool |
F
function
getPolicy
Parameters
typ
Returns
pkg/guard/policy.go:42-54
func getPolicy(typ reflect.Type) *CompiledPolicy
{
if val, ok := policyCache.Load(typ); ok {
return val.(*CompiledPolicy)
}
// Double check
// sync.Map doesn't have double check locking built-in for LoadOrStore construction,
// but it's safe to compute twice and overwrite.
policy := compilePolicy(typ)
policyCache.Store(typ, policy)
return policy
}
F
function
compilePolicy
Parameters
typ
Returns
pkg/guard/policy.go:56-119
func compilePolicy(typ reflect.Type) *CompiledPolicy
{
// Use the global parser (which has its own cache for FieldMeta, but we go further)
fields := globalParser.ParseType(typ)
policy := &CompiledPolicy{
StaticRules: make(map[string]map[string]bool),
DynamicRules: make([]DynamicRule, 0),
}
for _, meta := range fields {
permissions := meta.GetAll("can") // Actions
roles := meta.GetAll("role") // Roles
// Check for dynamic roles
isDynamicRole := false
staticRoles := make([]string, 0, len(roles))
for _, r := range roles {
if r == "*" {
isDynamicRole = true
} else {
staticRoles = append(staticRoles, r)
}
}
// If we have permissions, we map them
if len(permissions) > 0 {
for _, action := range permissions {
// Static roles part
if len(staticRoles) > 0 {
if policy.StaticRules[action] == nil {
policy.StaticRules[action] = make(map[string]bool)
}
// Also "wildcard action" handling?
// If action is "*", it applies to ALL actions requested runtime.
// We store it as "*" key.
for _, r := range staticRoles {
policy.StaticRules[action][r] = true
}
}
}
// Dynamic roles part
if isDynamicRole {
policy.DynamicRules = append(policy.DynamicRules, DynamicRule{
FieldIndex: meta.Index,
Actions: permissions,
IsWildcard: false, // role is wildcard, action is specific
})
}
} else {
// Case: "role:admin" but no "can".
// Does this imply "can everything"?
// Original code:
// if meta.GetAll("can") is empty, verify?
// Original loop: `permissions := meta.GetAll("can"); if len == 0 { continue }`
// So if no "can" is specified, the "role" tag is ignored for authorization checks?
// Yes, original code skipped if len(permissions) == 0.
}
}
return policy
}
F
function
extractRoles
Parameters
val
userID
string
Returns
[]string
pkg/guard/policy.go:215-245
func extractRoles(val reflect.Value, userID string) []string
{
var roles []string
// Handle Map, String, Slice
if val.Kind() == reflect.Map {
for _, key := range val.MapKeys() {
if checker.IsMatch(key, userID) {
roleVal := val.MapIndex(key)
if roleVal.Kind() == reflect.String {
roles = append(roles, roleVal.String())
} else if roleVal.Kind() == reflect.Slice || roleVal.Kind() == reflect.Array {
for i := 0; i < roleVal.Len(); i++ {
rv := roleVal.Index(i)
if rv.Kind() == reflect.String {
roles = append(roles, rv.String())
}
}
}
}
}
} else if val.Kind() == reflect.String {
roles = append(roles, val.String())
} else if val.Kind() == reflect.Slice {
for i := 0; i < val.Len(); i++ {
rv := val.Index(i)
if rv.Kind() == reflect.String {
roles = append(roles, rv.String())
}
}
}
return roles
}
S
struct
Guard
Guard provides the authorization engine.
pkg/guard/guard.go:15-15
type Guard struct
Methods
GetRoles
Method
GetRoles returns all roles resolved for the identity on the resource.
Parameters
user
Identity
resource
any
Returns
[]string
error
func (*Guard) GetRoles(user Identity, resource any) ([]string, error)
{
if user == nil {
return nil, errors.New("identity is nil")
}
if resource == nil {
return nil, errors.New("resource is nil")
}
val := reflect.ValueOf(resource)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, errors.New("resource must be a struct or pointer to struct")
}
// We need to support GetRoles even if the new policy logic is action-centric.
// But `GetRoles` is about "What roles does this user have on this resource?".
// The new Policy tracks logic per ACTION.
// However, we can inspect the policy to see which roles match.
// OR we can keep the old logic for GetRoles?
// But optimizing GetRoles is also important if used frequently.
// But `Can` is the main entry point.
// Let's implement GetRoles by checking all possible roles in the policy against the user.
policy := getPolicy(val.Type())
// Collect matching roles
matchedRoles := make(map[string]bool)
// Check user's explicit roles against any static role used in policy?
// User has roles [A, B].
// If resource allows A for action Read, then user has role A on this resource.
// This is vague.
// `GetRoles` usually returns roles that match dynamic criteria + static ones?
// Original logic: "If `role:admin` is on field, and user has `admin` global role, then user has `admin` on resource."
userRoles := user.GetRoles()
userRolesMap := make(map[string]bool, len(userRoles))
for _, r := range userRoles {
userRolesMap[r] = true
}
// 1. Static Roles from Policy
// We don't have a simple list of "All Static Roles" in compiled policy, but we have them in `StaticRules`.
for _, roleMap := range policy.StaticRules {
for r := range roleMap {
if r == "*" {
continue
}
if userRolesMap[r] {
matchedRoles[r] = true
}
}
}
// 2. Dynamic Roles
for _, rule := range policy.DynamicRules {
fieldVal := val.Field(rule.FieldIndex)
roles := extractRoles(fieldVal, user.GetID())
for _, r := range roles {
// If dynamic role (e.g. from DB) matches user's ID or is in user's roles?
// `role:*` extraction logic in original checked `fieldVal` against `user.GetID()`.
// Wait, the new `extractRoles` logic I wrote uses `checker.IsMatch(key, userID)`.
// So it extracts roles if the KEY matches the user.
// e.g. `role:*` on `map[string]string`. User ID "123". Map["123"] = "owner".
// Then "owner" is extracted.
// Then we check if user HAS role "owner"?
// Original logic: `userRoles[roleVal.String()] = true`.
// Yes, so if extracted role is in User's roles, we add it. Or is the extracted string THE role the user has?
// If `role:*` resolves to "owner", it means "The user with ID matching the key HAS the role 'owner' on this resource".
// So we add "owner" to `matchedRoles`.
// We DO NOT check if user already has "owner" in global traits.
// Validated against original lines 92: `userRoles[roleVal.String()] = true`.
matchedRoles[r] = true
}
}
roles := make([]string, 0, len(matchedRoles))
for r := range matchedRoles {
roles = append(roles, r)
}
return roles, nil
}
Can
Method
Can checks if the identity is allowed to perform the action on the resource.
Parameters
Returns
error
func (*Guard) Can(user Identity, resource any, action string) error
{
if user == nil {
return errors.New("identity is nil")
}
if resource == nil {
return errors.New("resource is nil")
}
val := reflect.ValueOf(resource)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return errors.New("resource must be a struct or pointer to struct")
}
policy := getPolicy(val.Type())
return policy.Evaluate(user, val, action)
}
F
function
NewGuard
NewGuard creates a new guard engine.
Returns
pkg/guard/guard.go:18-20
func NewGuard() *Guard
{
return &Guard{}
}
F
function
Can
Can checks if the identity is allowed to perform the action on the resource.
Parameters
Returns
error
pkg/guard/guard.go:133-135
func Can(user Identity, resource any, action string) error
{
return NewGuard().Can(user, resource, action)
}