guard API

guard

package

API reference for the guard package.

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

user Identity
resourceVal reflect.Value
action string

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

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

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

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
}
I
interface

Identity

Identity represents the actor trying to access a resource.

pkg/guard/guard.go:9-12
type Identity interface

Methods

GetID
Method

Returns

string
func GetID(...)
GetRoles
Method

Returns

[]string
func GetRoles(...)
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

user Identity
resource any
action string

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

user
resource
any
action
string

Returns

error
pkg/guard/guard.go:133-135
func Can(user Identity, resource any, action string) error

{
	return NewGuard().Can(user, resource, action)
}