guard API

guard

package

API reference for the guard package.

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)
}
S
struct

CompiledPolicy

CompiledPolicy holds pre-compiled authorization rules for a resource type.

pkg/guard/policy.go:18-23
type CompiledPolicy struct

Methods

Evaluate
Method

Parameters

user Identity
resourceVal reflect.Value
action string

Returns

error
func (*CompiledPolicy) Evaluate(user Identity, resourceVal reflect.Value, action string) error
{
	allowed := false
	ruleFound := false

	checkStatic := func(act string) {
		if allowedRoles, ok := p.StaticRules[act]; ok {
			ruleFound = true
			userRoles := user.GetRoles()
			for _, ur := range userRoles {
				if allowedRoles[ur] {
					allowed = true
					return
				}
				if allowedRoles["*"] {
					allowed = true
					return
				}
			}
		}
	}

	checkStatic(action)
	if allowed {
		return nil
	}
	checkStatic("*")
	if allowed {
		return nil
	}

	ruleIndices := p.actionIndex[action]
	rRuleIndices := make([]int, len(ruleIndices))
	copy(rRuleIndices, ruleIndices)
	rRuleIndices = append(rRuleIndices, p.wildcardIndex...)

	for _, idx := range rRuleIndices {
		if idx >= len(p.DynamicRules) {
			continue
		}
		rule := p.DynamicRules[idx]
		ruleFound = true
		fieldVal := resourceVal.Field(rule.FieldIndex)
		dynamicRoles := extractRoles(fieldVal, user.GetID())
		userRoles := user.GetRoles()

		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
actionIndex map[string][]int
wildcardIndex []int
S
struct

DynamicRule

DynamicRule maps a dynamic role field to the actions it governs.

pkg/guard/policy.go:26-31
type DynamicRule struct

Fields

Name Type Description
FieldIndex int
Actions []string
FieldType reflect.Type
FieldKind reflect.Kind
F
function

getPolicy

Parameters

Returns

pkg/guard/policy.go:33-40
func getPolicy(typ reflect.Type) *CompiledPolicy

{
	if val, ok := policyCache.Load(typ); ok {
		return val.(*CompiledPolicy)
	}
	policy := compilePolicy(typ)
	policyCache.Store(typ, policy)
	return policy
}
F
function

compilePolicy

Parameters

Returns

pkg/guard/policy.go:42-100
func compilePolicy(typ reflect.Type) *CompiledPolicy

{
	fields := globalParser.ParseType(typ)

	policy := &CompiledPolicy{
		StaticRules:    make(map[string]map[string]bool),
		DynamicRules:   make([]DynamicRule, 0),
		actionIndex:    make(map[string][]int),
		wildcardIndex:  make([]int, 0),
	}

	for _, meta := range fields {
		permissions := meta.GetAll("can")
		roles := meta.GetAll("role")

		isDynamicRole := false
		staticRoles := make([]string, 0, len(roles))

		for _, r := range roles {
			if r == "*" {
				isDynamicRole = true
			} else {
				staticRoles = append(staticRoles, r)
			}
		}

		if len(permissions) > 0 {
			for _, action := range permissions {
				if len(staticRoles) > 0 {
					if policy.StaticRules[action] == nil {
						policy.StaticRules[action] = make(map[string]bool)
					}
					for _, r := range staticRoles {
						policy.StaticRules[action][r] = true
					}
				}
			}

			if isDynamicRole {
				dr := DynamicRule{
					FieldIndex: meta.Index,
					Actions:    permissions,
					FieldType:  meta.Type,
					FieldKind:  meta.Type.Kind(),
				}
				idx := len(policy.DynamicRules)
				policy.DynamicRules = append(policy.DynamicRules, dr)
				for _, action := range permissions {
					if action == "*" {
						policy.wildcardIndex = append(policy.wildcardIndex, idx)
					} else {
						policy.actionIndex[action] = append(policy.actionIndex[action], idx)
					}
				}
			}
		}
	}

	return policy
}
F
function

extractRoles

Parameters

userID
string

Returns

[]string
pkg/guard/policy.go:173-202
func extractRoles(val reflect.Value, userID string) []string

{
	var roles []string
	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
}