diff --git a/README.md b/README.md index 432f51b..0d79f0c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ First of all, you need to determine `who` is trying to do `what` and `with whom` ### Who? - Entity -Entity is a subject wich will be authorized to perform actions on a specific resources. +Entity is a subject which will be authorized to perform actions on specific resources. In most cases you will have only 1 entity in your application - user. @@ -16,15 +16,15 @@ In most cases you will have only 1 entity in your application - user. Action just represents name of this action (under the hood is just a type definition based on string). Each action is bound to specific entity. -Action can be create via `.NewAction(, )` method of some entity. +Action can be created via `.NewAction(, )` method of some entity. ### With whom? - Resource -Resource is a thing on wich action is supposed to be performed. +Resource is a thing on which action is supposed to be performed. ### Context -Pretty simple, isn't it? All this 3 things combined together represents an `Authorization Context`, since they are used to just show what actually happens during authorization. But this still won't be enough, cuz we also need to know what this specific entity is allowed to do, in other words - it's `Permissions`. +Pretty simple, isn't it? All these 3 things combined together represent an `Authorization Context`, since they are used to show what actually happens during authorization. But this still won't be enough, because we also need to know what this specific entity is allowed to do, in other words - it's `Permissions`. ### PERMISSIONS @@ -46,13 +46,13 @@ Permissions are represented via bitmask, so they are very fast to work with. The - Self Delete -I assume that there are no need to describe what each one of them should permit to do. +I assume that there is no need to describe what each one of them should permit to do. -But how to store all this permissions? Of course we could just left it as a bitmask (which is just a number), but it will be really hard to maintain. So we need more convenient way to represent all this permissions and for that we will use `Roles`. +But how to store all these permissions? Of course we could just leave it as a bitmask (which is just a number), but it will be really hard to maintain. So we need a more convenient way to represent all these permissions and for that we will use `Roles`. ### ROLES -Each role consist of 2 parts: the name and permissions for this role. You can think of them as a named set of permissions. +Each role consists of 2 parts: the name and permissions for this role. You can think of them as a named set of permissions. ### Authorization diff --git a/action.go b/action.go index 71c921d..8c12f12 100644 --- a/action.go +++ b/action.go @@ -55,29 +55,29 @@ func NewActionGateRule(ctx *AuthorizationContext, effect ActionGateEffect, roles } // Validates that required fields are non-zero. -func (r *ActionGateRule) Validate() *Error { +func (r *ActionGateRule) Validate() error { if err := r.Effect.Validate(); err != nil { - return NewError("Invalid Action Gate Rule: " + err.Error()) + return errors.New("invalid action gate rule: " + err.Error()) } if r.Roles == nil || len(r.Roles) == 0 { - return NewError("Invalid Action Gate Rule: Roles are missing") + return errors.New("invalid action gate rule: roles are missing") } if r.Entity.name == "" { - return NewError("Invalid Action Gate Rule: Entity name is missing") + return errors.New("invalid action gate rule: entity name is missing") } if r.Action == "" { - return NewError("Invalid Action Gate Rule: Action is missing") + return errors.New("invalid action gate rule: action is missing") } var zeroResource Resource if r.Resource == zeroResource { - return NewError("Invalid Action Gate Rule: Resource is missing") + return errors.New("invalid action gate rule: resource is missing") } return nil } // Applies this rule for the given action with roles. // Returns true if default authorization must be skipped. -func (r *ActionGateRule) Apply(act Action, roles []Role) (bypassAuthz bool, err *Error) { +func (r *ActionGateRule) Apply(act Action, roles []Role) (bypassAuthz bool, err error) { if r.Roles != nil && r.Action != act { return false, nil } @@ -96,11 +96,11 @@ func (r *ActionGateRule) Apply(act Action, roles []Role) (bypassAuthz bool, err switch r.Effect { case DenyActionGateEffect: if matchRuleRoles { - return false, ActionDeniedByAGP + return false, ErrActionDeniedByAGP } case RequireActionGateEffect: if !matchRuleRoles { - return false, ActionDeniedByAGP + return false, ErrActionDeniedByAGP } case AllowActionGateEffect: if matchRuleRoles { @@ -134,7 +134,7 @@ func (agp ActionGatePolicy) GetRule(ctx *AuthorizationContext) (*ActionGateRule, // Adds new rule in police, will return error if rule // is either invalid, either already exist in policy -func (agp ActionGatePolicy) AddRule(rule *ActionGateRule) *Error { +func (agp ActionGatePolicy) AddRule(rule *ActionGateRule) error { if err := rule.Validate(); err != nil { return err } @@ -142,7 +142,7 @@ func (agp ActionGatePolicy) AddRule(rule *ActionGateRule) *Error { key := agp.keyFrom(&rule.Entity, rule.Action, &rule.Resource) if _, ok := agp.rules[key]; ok { - return NewError("Rule " + key + " already exist in Action Gate Policy") + return errors.New("rule " + key + " already exists in action gate policy") } agp.rules[key] = rule diff --git a/action_gate_policy_test.go b/action_gate_policy_test.go index bcf8694..c997fa3 100644 --- a/action_gate_policy_test.go +++ b/action_gate_policy_test.go @@ -30,7 +30,7 @@ func TestActionGatePolicy(t *testing.T) { // Test rule application bypass, err := rule.Apply(readAction, []Role{adminRole}) - if err != ActionDeniedByAGP { + if err != ErrActionDeniedByAGP { t.Errorf("Expected ActionDeniedByAGP, got %v", err) } if bypass { diff --git a/authorization.go b/authorization.go index 8d5fa66..0bc61a8 100644 --- a/authorization.go +++ b/authorization.go @@ -1,35 +1,51 @@ package rbac -type AuthzFunc func(Permissions, Permissions) *Error +// AuthzFunc checks user's permissions. +type AuthzFunc func(Permissions, Permissions) error -var authorize AuthzFunc = AuthorizeCRUDFunc +// RuleProvider can lookup ActionGate rules for provided context. +type RuleProvider interface { + GetRule(ctx *AuthorizationContext) (*ActionGateRule, bool) +} -// AuthorizationFunc checks user's permissions. -// -// This function is incapsulated in rbac package, so it can't be called directly, -// instead use "Authorize" function. -// -// AuthorizationFunc can be overridden via this function, to implement custom authorization logic. -// By default it uses the AuthorizeCRUDFunc function. +// Authorizer encapsulates authorization behavior. +type Authorizer struct { + authzFunc AuthzFunc +} + +// NewAuthorizer creates authorizer with default authorization function. +func NewAuthorizer() *Authorizer { + return &Authorizer{ + authzFunc: AuthorizeCRUDFunc, + } +} + +var defaultAuthorizer = NewAuthorizer() + +// SetAuthzFunc overrides default authorization function globally. func SetAuthzFunc(fn AuthzFunc) { + defaultAuthorizer.SetAuthzFunc(fn) +} + +// SetAuthzFunc overrides authorization function for this authorizer. +func (a *Authorizer) SetAuthzFunc(fn AuthzFunc) { if fn == nil { panic("authorization function can't be nil") } - - authorize = fn + a.authzFunc = fn } // Checks if the "permitted" permissions are sufficient to satisfy the "required" CRUD permissions. // // It returns an "InsufficientPermissions" error if any of the "required" permissions are not covered by the "permitted" permissions. -func AuthorizeCRUDFunc(required Permissions, permitted Permissions) *Error { +func AuthorizeCRUDFunc(required Permissions, permitted Permissions) error { // To verify that 'permitted' satisfies 'required' need to check // if all 1 bits in 'required' are set in 'permitted', // For that need to perform a bitwise AND between 'required' and 'permitted', // then verify if the result equals 'required'. // If (required & permitted) == required, all ones in required are present in permitted. if required&permitted != required { - return InsufficientPermissions + return ErrInsufficientPermissions } return nil @@ -38,13 +54,18 @@ func AuthorizeCRUDFunc(required Permissions, permitted Permissions) *Error { // Checks if the user has sufficient permissions to perform an action on this resource. // // Returns an error if any of the required permissions for the action are not covered by given roles. -func Authorize(ctx *AuthorizationContext, roles []Role, AGP *ActionGatePolicy) *Error { +func Authorize(ctx *AuthorizationContext, roles []Role, provider RuleProvider) error { + return defaultAuthorizer.Authorize(ctx, roles, provider) +} + +// Authorize checks authorization using provided rule provider. +func (a *Authorizer) Authorize(ctx *AuthorizationContext, roles []Role, provider RuleProvider) error { if !ctx.Entity.HasAction(ctx.Action) { - return EntityDoesNotHaveSuchAction + return ErrEntityDoesNotHaveSuchAction } - if AGP != nil { - if rule, ok := AGP.GetRule(ctx); ok { + if provider != nil { + if rule, ok := provider.GetRule(ctx); ok { bypass, err := rule.Apply(ctx.Action, roles) if err != nil { return err @@ -62,9 +83,9 @@ func Authorize(ctx *AuthorizationContext, roles []Role, AGP *ActionGatePolicy) * mergredPermissions |= role.Permissions } - if err := authorize(requiredPermissions, mergredPermissions); err == nil { - return nil + if err := a.authzFunc(requiredPermissions, mergredPermissions); err != nil { + return err } - return InsufficientPermissions + return nil } diff --git a/authorization_test.go b/authorization_test.go index 87eec34..63663a4 100644 --- a/authorization_test.go +++ b/authorization_test.go @@ -1,6 +1,7 @@ package rbac import ( + "errors" "testing" ) @@ -32,17 +33,25 @@ func TestAuthorizeCRUDFunc(t *testing.T) { func TestSetAuthzFunc(t *testing.T) { // Test custom function - customFunc := func(required, permitted Permissions) *Error { + customFunc := func(required, permitted Permissions) error { if required == 0 { - return NewError("custom error") + return errors.New("custom error") } return nil } SetAuthzFunc(customFunc) - err := authorize(0, ReadPermission) + + user := NewEntity("user") + action, _ := user.NewAction("test", 0) + resource := NewResource("res") + ctx := NewAuthorizationContext(&user, action, resource) + + err := Authorize(&ctx, nil, nil) if err == nil { t.Error("Expected custom error, got nil") + } else if err.Error() != "custom error" { + t.Errorf("Expected 'custom error', got %s", err.Error()) } // Reset and test panic @@ -107,13 +116,13 @@ func TestAuthorizeWithActionGatePolicy(t *testing.T) { // Test deny effect err := Authorize(&ctx, []Role{adminRole}, &agp) - if err != ActionDeniedByAGP { + if err != ErrActionDeniedByAGP { t.Errorf("Expected ActionDeniedByAGP, got %v", err) } // Test no rule applies err = Authorize(&ctx, []Role{userRole}, &agp) - if err != InsufficientPermissions { + if err != ErrInsufficientPermissions { t.Errorf("Expected InsufficientPermissions, got %v", err) } } diff --git a/cmd/main.go b/cmd/main.go index dd36c48..e258a49 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -80,10 +80,10 @@ func example() { } e := rbac.Authorize(&ctx, roles1, &agp) - // Error: Action has been denied by Action Gate Policy + // fmt.Println(e) e = rbac.Authorize(&ctx, roles2, &agp) - // Error: Insufficient permissions to perform this action + // Error: Action has been denied by Action Gate Policy fmt.Println(e) } diff --git a/common.go b/common.go index 109e576..75006ab 100644 --- a/common.go +++ b/common.go @@ -27,9 +27,8 @@ func load[T any, R loadable[T]](path string, postLoad func(*R)) (T, error) { return zero, err } defer func() { - if err = file.Close(); err != nil { - Debug.Log(err.Error()) - os.Exit(1) + if closeErr := file.Close(); closeErr != nil { + Debug.Log(closeErr.Error()) } }() diff --git a/error.go b/error.go index 32f2b6b..30af9bc 100644 --- a/error.go +++ b/error.go @@ -1,19 +1,9 @@ package rbac -type Error struct { - message string -} - -func (e *Error) Error() string { - return e.message -} - -func NewError(message string) *Error { - return &Error{message} -} +import "errors" var ( - InsufficientPermissions = NewError("Insufficient permissions to perform this action") - EntityDoesNotHaveSuchAction = NewError("Entity doesn't have such action") - ActionDeniedByAGP = NewError("Action has been denied by Action Gate Policy") + ErrInsufficientPermissions = errors.New("insufficient permissions to perform this action") + ErrEntityDoesNotHaveSuchAction = errors.New("entity doesn't have such action") + ErrActionDeniedByAGP = errors.New("action has been denied by action gate policy") ) diff --git a/error_test.go b/error_test.go index c93861c..1834637 100644 --- a/error_test.go +++ b/error_test.go @@ -5,34 +5,35 @@ import ( ) func TestError(t *testing.T) { - // Test custom error creation - err := NewError("test error") - if err.Error() != "test error" { - t.Errorf("Expected 'test error', got %s", err.Error()) - } - // Test error interface implementation - var errorInterface error = err + var errorInterface error = ErrInsufficientPermissions if errorInterface == nil { t.Error("Error should implement error interface") } // Test predefined errors - if InsufficientPermissions.Error() != "Insufficient permissions to perform this action" { - t.Error("InsufficientPermissions message incorrect") + if ErrInsufficientPermissions.Error() != "insufficient permissions to perform this action" { + t.Error("ErrInsufficientPermissions message incorrect") + } + + if ErrEntityDoesNotHaveSuchAction.Error() != "entity doesn't have such action" { + t.Error("ErrEntityDoesNotHaveSuchAction message incorrect") } - if EntityDoesNotHaveSuchAction.Error() != "Entity doesn't have such action" { - t.Error("EntityDoesNotHaveSuchAction message incorrect") + if ErrActionDeniedByAGP.Error() != "action has been denied by action gate policy" { + t.Error("ErrActionDeniedByAGP message incorrect") } - if ActionDeniedByAGP.Error() != "Action has been denied by Action Gate Policy" { - t.Error("ActionDeniedByAGP message incorrect") + // Test legacy support + if ErrInsufficientPermissions.Error() != "insufficient permissions to perform this action" { + t.Error("Legacy InsufficientPermissions message incorrect") } - // Test error comparison - _ = NewError("same message") - _ = NewError("same message") - // Note: Different error instances will never be equal - // This test documents the current behavior + if ErrEntityDoesNotHaveSuchAction.Error() != "entity doesn't have such action" { + t.Error("Legacy EntityDoesNotHaveSuchAction message incorrect") + } + + if ErrActionDeniedByAGP.Error() != "action has been denied by action gate policy" { + t.Error("Legacy ActionDeniedByAGP message incorrect") + } } diff --git a/host.go b/host.go index 5a8db01..3bf990e 100644 --- a/host.go +++ b/host.go @@ -1,5 +1,7 @@ package rbac +import "errors" + // Host originaly desined for applications with microservice architectures. // // Host helps to define roles and schemas for each service in your app. @@ -10,13 +12,13 @@ type Host struct { Schemas []Schema } -func (h *Host) GetSchema(ID string) (*Schema, *Error) { +func (h *Host) GetSchema(ID string) (*Schema, error) { if h == nil { - return nil, NewError("RBAC schema is not defined") + return nil, errors.New("RBAC schema is not defined") } if ID == "" { - return nil, NewError("Missing schema id") + return nil, errors.New("missing schema id") } for _, schema := range h.Schemas { @@ -25,7 +27,7 @@ func (h *Host) GetSchema(ID string) (*Schema, *Error) { } } - return nil, NewError("Schema with id \"" + ID + "\" wasn't found") + return nil, errors.New("schema with id \"" + ID + "\" wasn't found") } // Merges permissions from schema specific roles with global roles. diff --git a/integration_test.go b/integration_test.go index 9cb4e28..1f5c670 100644 --- a/integration_test.go +++ b/integration_test.go @@ -59,7 +59,7 @@ func TestAuthorizationWithActionGatePolicy(t *testing.T) { // Test deny effect err := Authorize(&ctx, []Role{adminRole}, &agp) - if err != ActionDeniedByAGP { + if err != ErrActionDeniedByAGP { t.Errorf("Expected ActionDeniedByAGP, got %v", err) } diff --git a/raw.go b/raw.go index b4ee5ba..0ef12d6 100644 --- a/raw.go +++ b/raw.go @@ -2,19 +2,18 @@ package rbac import ( "fmt" - "slices" ) -// TODO add partional loading (to be able for example to init all actions in code, but load AGP from config) +// TODO add partial loading (to be able for example to init all actions in code, but load AGP from config) -// "raw" structs are design to be used by host and schema to be able being initialized from files. +// "raw" structs are designed to be used by host and schema to be able to be initialized from files. // They are more user-friendly, but also more "heavy". // // So for example - rawPermissions is just a struct which consists of flags, // instead original Permissions is bitmask. -// Of course rawPermissions more convenient and readable, but also more slow. +// Of course rawPermissions are more convenient and readable, but also slower. // For example: -// For authz using Permissions used bitwise operations which are extrimely fast, +// For authz using Permissions used bitwise operations which are extremely fast, // but using rawPermissions the only way is to check all flag one-by-one in 'if' statements. type rawPermissions struct { @@ -110,52 +109,11 @@ func normalizeRoles(rawRoles []*rawRole) []Role { } func normalizeDefaultRoles(roles []Role, defaultRolesNames []string) ([]Role, error) { - defaultRoles := make([]Role, 0, len(defaultRolesNames)) - - for _, role := range roles { - if slices.Contains(defaultRolesNames, role.Name) { - defaultRoles = append(defaultRoles, role) - } - } - - // TODO deduplicate that? (check validateDefaultRoles()) - if len(defaultRoles) != len(defaultRolesNames) { - outer: - for _, roleName := range defaultRolesNames { - for _, role := range defaultRoles { - if roleName == role.Name { - continue outer - } - - } - return nil, fmt.Errorf( - "Invalid role '%s'. This role doesn't exist in Schema roles", - roleName, - ) - } - } - - return defaultRoles, nil + roleMap := buildRoleMap(roles) + return rolesByNames(roleMap, defaultRolesNames) } // Used to get slice of normalized elements using their raw representations. -func getNormalFrom[T any](raw []string, normal []T, cmp func(a string, b T) bool) ([]T, error) { - result := make([]T, 0, len(raw)) - -main_loop: - for _, rawItem := range raw { - for _, normalItem := range normal { - if cmp(rawItem, normalItem) { - result = append(result, normalItem) - continue main_loop - } - } - return nil, fmt.Errorf("\"%s\" doesn't exist", rawItem) - } - - return result, nil -} - func normalizeActionGatePolicy( schemaEntities []Entity, schemaRoles []Role, @@ -166,17 +124,28 @@ func normalizeActionGatePolicy( agp := NewActionGatePolicy() - for _, rawRule := range rawAgp { - var ruleResource Resource - var zeroResource Resource + entityMap := make(map[string]Entity, len(schemaEntities)) + entityActions := make(map[string]map[string]Action, len(schemaEntities)) + for _, entity := range schemaEntities { + entityMap[entity.name] = entity - for _, resource := range schemaResources { - if resource.name == rawRule.On { - ruleResource = resource - break - } + actionMap := make(map[string]Action, len(entity.actions)) + for action := range entity.actions { + actionMap[action.String()] = action } - if ruleResource == zeroResource { + entityActions[entity.name] = actionMap + } + + resourceMap := make(map[string]Resource, len(schemaResources)) + for _, resource := range schemaResources { + resourceMap[resource.name] = resource + } + + roleMap := buildRoleMap(schemaRoles) + + for _, rawRule := range rawAgp { + ruleResource, ok := resourceMap[rawRule.On] + if !ok { return zero, fmt.Errorf("Resource %s doesn't exist in the schema resources", rawRule.On) } @@ -184,38 +153,33 @@ func normalizeActionGatePolicy( return zero, fmt.Errorf("Rule missing entity(-s) for the %s resource", ruleResource.name) } - ruleEntities, err := getNormalFrom(rawRule.For, schemaEntities, func(a string, b Entity) bool { - return a == b.name - }) - if err != nil { - return zero, fmt.Errorf("Failed to get normalized entity - %s", err.Error()) + if rawRule.Doing == nil || len(rawRule.Doing) == 0 { + return zero, fmt.Errorf("Rule missing action(-s) for the %s resource", ruleResource.name) } - ruleRoles, err := getNormalFrom(rawRule.Having, schemaRoles, func(a string, b Role) bool { - return a == b.Name - }) - if err != nil { - return zero, fmt.Errorf("Failed to get normalized roles - %s", err.Error()) - } - - for _, ruleEntity := range ruleEntities { - if rawRule.Doing == nil || len(rawRule.Doing) == 0 { - return zero, fmt.Errorf( - "Rule missing action(-s) for the %s entity on the %s resource", - ruleEntity.name, ruleResource.name, - ) + var ruleRoles []Role + if len(rawRule.Having) > 0 { + roles, err := rolesByNames(roleMap, rawRule.Having) + if err != nil { + return zero, fmt.Errorf("Failed to get normalized roles - %s", err.Error()) } + ruleRoles = roles + } - actions := make([]Action, 0, len(ruleEntity.actions)) - for action := range ruleEntity.actions { - actions = append(actions, action) + for _, entityName := range rawRule.For { + ruleEntity, ok := entityMap[entityName] + if !ok { + return zero, fmt.Errorf("Failed to get normalized entity - \"%s\" doesn't exist", entityName) } - ruleActions, err := getNormalFrom(rawRule.Doing, actions, func(a string, b Action) bool { - return a == b.String() - }) - if err != nil { - return zero, fmt.Errorf("Failed to get normalized action for the %s entity - %s", ruleEntity.name, err.Error()) + actionMap := entityActions[entityName] + ruleActions := make([]Action, 0, len(rawRule.Doing)) + for _, actionName := range rawRule.Doing { + action, ok := actionMap[actionName] + if !ok { + return zero, fmt.Errorf("Failed to get normalized action for the %s entity - \"%s\" doesn't exist", ruleEntity.name, actionName) + } + ruleActions = append(ruleActions, action) } for _, ruleAction := range ruleActions { @@ -253,15 +217,15 @@ func normalizeEntities(rawEntities []*rawEntity) []Entity { } func normalizeResources(rawResources []string) []Resource { - resorces := make([]Resource, len(rawResources)) + resources := make([]Resource, 0, len(rawResources)) for _, rawResource := range rawResources { - resorces = append(resorces, Resource{ + resources = append(resources, Resource{ name: rawResource, }) } - return resorces + return resources } // Creates new Schema based on self. diff --git a/roles.go b/roles.go new file mode 100644 index 0000000..ea65370 --- /dev/null +++ b/roles.go @@ -0,0 +1,29 @@ +package rbac + +import "fmt" + +// creates a map for quick lookups by role name. +func buildRoleMap(roles []Role) map[string]Role { + roleMap := make(map[string]Role, len(roles)) + for _, role := range roles { + roleMap[role.Name] = role + } + return roleMap +} + +// returns a slice of roles matching the provided names. +// Returns an error if any of the names do not exist in the role map. +func rolesByNames(roleMap map[string]Role, names []string) ([]Role, error) { + result := make([]Role, 0, len(names)) + + for _, name := range names { + role, ok := roleMap[name] + if !ok { + return nil, fmt.Errorf("Invalid role '%s'. This role doesn't exist in Schema roles", name) + } + + result = append(result, role) + } + + return result, nil +} diff --git a/schema.go b/schema.go index c575765..865fcc2 100644 --- a/schema.go +++ b/schema.go @@ -1,5 +1,7 @@ package rbac +import "errors" + type Schema struct { ID string Roles []Role @@ -18,14 +20,14 @@ func NewSchema(id string, roles []Role, defaultRoles []Role, agp ActionGatePolic } } -func (schema *Schema) ParseRole(roleName string) (Role, *Error) { +func (schema *Schema) ParseRole(roleName string) (Role, error) { for _, role := range schema.Roles { if role.Name == roleName { return role, nil } } - return Role{}, NewError("\"" + schema.ID + "\" schema doesn't have \"" + roleName + "\" role") + return Role{}, errors.New("schema \"" + schema.ID + "\" doesn't have role \"" + roleName + "\"") } // Reads and parses RBAC schema from file at the specified path. diff --git a/validate.go b/validate.go index 0deee47..5ce2dc3 100644 --- a/validate.go +++ b/validate.go @@ -3,44 +3,53 @@ package rbac import ( "errors" "fmt" - "slices" ) func validateDefaultRoles(roles []Role, defaultRoles []Role) error { -outer: - for _, defaultRole := range defaultRoles { - for _, role := range roles { - if defaultRole.Name == role.Name { - continue outer - } + roleMap := buildRoleMap(roles) + for _, defaultRole := range defaultRoles { + if _, exists := roleMap[defaultRole.Name]; !exists { + return fmt.Errorf( + "Invalid role '%s'. This role doesn't exist in Schema roles", + defaultRole.Name, + ) } - - return fmt.Errorf( - "Invalid role '%s'. This role doesn't exist in Schema roles", - defaultRole.Name, - ) } return nil } func validateAGP(schema *Schema) error { + // Create lookup maps for O(1) validation + entityMap := make(map[string]bool) + for _, entity := range schema.Entities { + entityMap[entity.name] = true + } + + resourceMap := make(map[string]bool) + for _, resource := range schema.Resources { + resourceMap[resource.name] = true + } + + roleMap := make(map[string]bool) + for _, role := range schema.Roles { + roleMap[role.Name] = true + } + for ruleName, rule := range schema.ActionGatePolicy.rules { if err := rule.Effect.Validate(); err != nil { return fmt.Errorf("Invalid Action Gate Policy rule %s in the %s schema - %s", ruleName, schema.ID, err.Error()) } - if !slices.ContainsFunc(schema.Entities, func(v Entity) bool { - return v.name == rule.Entity.name - }) { + if !entityMap[rule.Entity.name] { return fmt.Errorf( "Invalid Action Gate Policy rule %s - Entity %s doesn't exist in the %s schema", ruleName, rule.Entity.name, schema.ID, ) } - if !slices.Contains(schema.Resources, rule.Resource) { + if !resourceMap[rule.Resource.name] { return fmt.Errorf( "Invalid Action Gate Policy rule %s - resource %s doesn't exist in the %s schema", ruleName, rule.Resource.name, schema.ID, @@ -48,7 +57,7 @@ func validateAGP(schema *Schema) error { } for _, ruleRole := range rule.Roles { - if !slices.Contains(schema.Roles, ruleRole) { + if !roleMap[ruleRole.Name] { return fmt.Errorf( "Invalid Action Gate Policy rule %s - Role %s doesn't exist in the %s schema", ruleName, ruleRole.Name, schema.ID, @@ -56,12 +65,7 @@ func validateAGP(schema *Schema) error { } } - actions := []Action{} - for action := range rule.Entity.actions { - actions = append(actions, action) - } - - if !slices.Contains(actions, rule.Action) { + if !rule.Entity.HasAction(rule.Action) { return fmt.Errorf( "Invalid Action Gate Policy rule %s - Action %s doesn't exist in the %s schema", ruleName, rule.Action, schema.ID, @@ -99,19 +103,8 @@ func ValidateHost(host *Host) error { return err } - outer: - for _, defaultRole := range schema.DefaultRoles { - for _, role := range schema.Roles { - if role.Name == defaultRole.Name { - continue outer - } - } - - return fmt.Errorf( - "Invalid default role '%s' in '%s' schema: there are no such role in this schema", - defaultRole.Name, - schema.ID, - ) + if err := validateDefaultRoles(schema.Roles, schema.DefaultRoles); err != nil { + return err } }