├── .githooks ├── pre-commit └── prepare-commit-msg ├── go.mod ├── .gitignore ├── access_manager_deps.go ├── internal ├── utils │ ├── slices_utils.go │ ├── slices_utils_test.go │ ├── type_utils.go │ └── type_utils_test.go ├── examples │ ├── models.go │ ├── basic_usage.go │ ├── policy_example.yaml │ ├── policy_example.json │ ├── error_handling_example.go │ └── policy_example.go └── tests │ ├── models_test.go │ ├── custom_condition_test.go │ ├── policies_test.go │ ├── test_policy.yaml │ ├── test_policy.json │ └── integration_test.go ├── .github └── workflows │ ├── refresh-pkg.yml │ ├── dev.yml │ ├── master.yml │ └── v2.yml ├── policy_definition.go ├── storage_adapter.go ├── adapters ├── errors.go ├── in_memory_adapter.go ├── in_memory_adapter_test.go ├── mocks_test.go ├── file_adapter_deps.go ├── file_adapter.go └── file_adapter_test.go ├── subject.go ├── CHANGELOG.md ├── resource.go ├── access_request.go ├── LICENSE ├── permission.go ├── role.go ├── go.sum ├── value_descriptor.go ├── permission_test.go ├── condition_empty.go ├── value_source.go ├── condition_equal.go ├── role_test.go ├── mocks_test.go ├── condition_empty_test.go ├── value_source_test.go ├── value_descriptor_test.go ├── condition_equal_test.go ├── condition.go ├── authorization_errors.go ├── common_errors.go ├── access_manager.go ├── condition_test.go ├── policy_manager.go ├── policy_manager_test.go └── access_manager_test.go /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | golangci-lint run "$@" 4 | go test ./... 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/el-mike/restrict/v2 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | gopkg.in/yaml.v3 v3.0.1 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | 4 | rules.txt 5 | 6 | **/coverage.out 7 | **/debug.test 8 | 9 | internal/examples/basic_usage_test.go 10 | internal/examples/error_handling_example_test.go 11 | -------------------------------------------------------------------------------- /access_manager_deps.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // PolicyProvider - interface for an entity that will provide Role configuration 4 | // for AccessProvider. 5 | type PolicyProvider interface { 6 | GetRole(roleID string) (*Role, error) 7 | } 8 | -------------------------------------------------------------------------------- /internal/utils/slices_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func StringSliceContains(source []string, target string) bool { 4 | for _, s := range source { 5 | if s == target { 6 | return true 7 | } 8 | } 9 | 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/refresh-pkg.yml: -------------------------------------------------------------------------------- 1 | name: refresh-pkg 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | tags: 8 | - 'v[0-9]+.[0-9]+.[0-9]+' 9 | - '**/v[0-9]+.[0-9]+.[0-9]+' 10 | 11 | jobs: 12 | build: 13 | name: Renew documentation 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Pull new module version 17 | uses: andrewslotin/go-proxy-pull-action@master 18 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: dev 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /.github/workflows/v2.yml: -------------------------------------------------------------------------------- 1 | name: v2 2 | 3 | on: 4 | push: 5 | branches: [ v2 ] 6 | pull_request: 7 | branches: [ v2 ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /policy_definition.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // PolicyDefinition - describes a model of Roles and Permissions that 4 | // are defined for the domain. 5 | type PolicyDefinition struct { 6 | // PermissionPresets - a map of Permission presets. 7 | PermissionPresets PermissionPresets `json:"permissionPresets,omitempty" yaml:"permissionPresets,omitempty"` 8 | // Roles - collection of Roles used in the domain. 9 | Roles Roles `json:"roles" yaml:"roles"` 10 | } 11 | -------------------------------------------------------------------------------- /storage_adapter.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // StorageAdapter - interface for an entity that will provide persistence 4 | // logic for PolicyDefinition. 5 | type StorageAdapter interface { 6 | // LoadPolicy - loads and returns PolicyDefinition from underlying 7 | // storage provider. 8 | LoadPolicy() (*PolicyDefinition, error) 9 | 10 | // SavePolicy - saves PolicyDefinition in underlying storage provider. 11 | SavePolicy(policy *PolicyDefinition) error 12 | } 13 | -------------------------------------------------------------------------------- /internal/examples/models.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | type User struct { 4 | ID string 5 | } 6 | 7 | // Subject interface implementation. 8 | func (u *User) GetRoles() []string { 9 | return []string{"User"} 10 | } 11 | 12 | type Conversation struct { 13 | ID string 14 | CreatedBy string 15 | Participants []string 16 | MessagesCount int 17 | Active bool 18 | } 19 | 20 | // Resource interface implementation. 21 | func (c *Conversation) GetResourceName() string { 22 | return "Conversation" 23 | } 24 | -------------------------------------------------------------------------------- /internal/utils/slices_utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type slicesUtilsSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestSlicesUtilsSuite(t *testing.T) { 15 | suite.Run(t, new(slicesUtilsSuite)) 16 | } 17 | 18 | func (s *slicesUtilsSuite) TestStringSliceContains() { 19 | testSlice := []string{"one", "two", "three", "three"} 20 | 21 | assert.True(s.T(), StringSliceContains(testSlice, "one")) 22 | assert.False(s.T(), StringSliceContains(testSlice, "four")) 23 | } 24 | -------------------------------------------------------------------------------- /adapters/errors.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import "fmt" 4 | 5 | // FileTypeNotSupportedError - thrown when FileAdapter is used with inappropriate file type. 6 | type FileTypeNotSupportedError struct { 7 | fileType string 8 | } 9 | 10 | // newFileTypeNotSupportedError - returns new FileTypeNotSupportedError instance. 11 | func newFileTypeNotSupportedError(fileType string) *FileTypeNotSupportedError { 12 | return &FileTypeNotSupportedError{ 13 | fileType: fileType, 14 | } 15 | } 16 | 17 | // Error - error interface implementation. 18 | func (e *FileTypeNotSupportedError) Error() string { 19 | return fmt.Sprintf("file type: \"%s\" is not supported", e.fileType) 20 | } 21 | -------------------------------------------------------------------------------- /subject.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // Subject - interface that has to be implemented by any entity 4 | // which authorization needs to be checked. 5 | type Subject interface { 6 | // GetRoles - returns a Subject's role. 7 | GetRoles() []string 8 | } 9 | 10 | // baseSubject - Subject implementation, to be used when proper entity 11 | // is impossible or not feasible to obtain. 12 | type baseSubject struct { 13 | roles []string 14 | } 15 | 16 | // GetRoles - Subject interface implementation. 17 | func (bs *baseSubject) GetRoles() []string { 18 | return bs.roles 19 | } 20 | 21 | // UseSubject - returns baseSubject instance. 22 | func UseSubject(roles []string) *baseSubject { 23 | return &baseSubject{ 24 | roles, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | - Allows Subject to have multiple roles 4 | - Errors API changes: 5 | - `AccessDeniedError` now has `Reasons` property, containing one or more `PermissionErrors` describing what exactly went wrong 6 | for every Role of the Subject and Action in the Request 7 | - `PermissionError` now has `ConditionErrors` property, containing zero or more `ConditionNotSatisfiedErrors` describing `Condition` failures for given Permissions 8 | - properties of `AccessDeniedError` and `PermissionError` are now public 9 | - Allows to perform complete validation when `AccessRequest.CompleteValidation` is set to true 10 | - Defaults to false (fail-early strategy) 11 | - When using `CompleteValidation`, all Policy errors will be collected and returned at once, instead of failing on 12 | first encountered Policy error 13 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // Resource - interface that needs to be implemented by any entity 4 | // which acts as a resource in the system. 5 | type Resource interface { 6 | // GetResourceName - returns a Resource's name. Should be the same as the one 7 | // used in PolicyDefinition. 8 | GetResourceName() string 9 | } 10 | 11 | // baseResource - Resource implementation, to be used when proper Resource 12 | // is impossible or not feasible to obtain. 13 | type baseResource struct { 14 | name string 15 | } 16 | 17 | // GetResourceName - Resource interface implementation. 18 | func (br *baseResource) GetResourceName() string { 19 | return br.name 20 | } 21 | 22 | // UseResource - returns baseResource instance. 23 | func UseResource(name string) *baseResource { 24 | return &baseResource{ 25 | name: name, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /adapters/in_memory_adapter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import "github.com/el-mike/restrict/v2" 4 | 5 | // InMemoryAdapter - StorageAdapter implementation, providing in-memory persistence. 6 | type InMemoryAdapter struct { 7 | policy *restrict.PolicyDefinition 8 | } 9 | 10 | // NewInMemoryAdapter - returns new InMemoryAdapter instance. 11 | func NewInMemoryAdapter(policy *restrict.PolicyDefinition) *InMemoryAdapter { 12 | return &InMemoryAdapter{ 13 | policy: policy, 14 | } 15 | } 16 | 17 | // LoadPolicy - returns policy from memory. 18 | func (ia *InMemoryAdapter) LoadPolicy() (*restrict.PolicyDefinition, error) { 19 | return ia.policy, nil 20 | } 21 | 22 | // SavePolicy - saves policy to memory. 23 | func (ia *InMemoryAdapter) SavePolicy(policy *restrict.PolicyDefinition) error { 24 | ia.policy = policy 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /.githooks/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMIT_MSG_FILENAME=$1 4 | 5 | MESSAGE_PREFIXES=("feat" "fix" "chore") 6 | MESSAGE_PREFIX_SEPARATOR=":" 7 | 8 | message="$(cat $COMMIT_MSG_FILENAME)" 9 | 10 | if [[ $message =~ [A-Z] ]]; then 11 | printf "Message should be lowercase!\n" 12 | exit 1 13 | fi 14 | 15 | # Note that IFS will only be set to ':' for read command. 16 | IFS=':' read -r -a parts <<< "$message" 17 | 18 | if [ ${#parts[@]} != 2 ]; then 19 | printf "Incorrect format! Message should be formatted as follows:\n" 20 | printf ": \n" 21 | exit 1 22 | fi 23 | 24 | prefix=${parts[0]} 25 | 26 | prefix_found=0 27 | 28 | for current_prefix in ${MESSAGE_PREFIXES[@]}; do 29 | if [ "$prefix" == "$current_prefix" ]; then 30 | prefix_found=1 31 | fi 32 | done 33 | 34 | if [ $prefix_found == 0 ]; then 35 | printf "Message prefix \"$prefix\" is incorrect!\n" 36 | printf "Possible prefixes are: ${MESSAGE_PREFIXES[0]}, ${MESSAGE_PREFIXES[1]}, ${MESSAGE_PREFIXES[2]}\n" 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /access_request.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // Context - alias type for a map of any values. 4 | type Context map[string]interface{} 5 | 6 | // AccessRequest - describes a Subject's intention to perform some Actions against 7 | // given Resource. 8 | type AccessRequest struct { 9 | // Subject - subject (typically a user) that wants to perform given Actions. 10 | // Needs to implement Subject interface. 11 | Subject Subject 12 | // Resource - resource that given Subject wants to interact with. 13 | // Needs to implement Resource interface. 14 | Resource Resource 15 | // Actions - list of operations Subject wants to perform on given Resource. 16 | Actions []string 17 | // Context - map of any additional values needed while checking the access. 18 | Context Context 19 | // SkipConditions - allows to skip Conditions while checking the access. 20 | SkipConditions bool 21 | // CompleteValidation - when true, validation will not return early, and all possible errors 22 | // will be returned, including all Conditions checks. 23 | CompleteValidation bool 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michał Huras 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/tests/models_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | const ( 4 | BasicUserRole = "BasicUser" 5 | UserRole = "User" 6 | AdminRole = "Admin" 7 | 8 | UserResource = "User" 9 | ConversationResource = "Conversation" 10 | MessageResource = "Message" 11 | ) 12 | 13 | type User struct { 14 | ID string 15 | 16 | Roles []string 17 | } 18 | 19 | // Subject interface implementation. 20 | func (u *User) GetRoles() []string { 21 | return u.Roles 22 | } 23 | 24 | // Resource interface implementation. User can be both Subject and Resource. 25 | func (u *User) GetResourceName() string { 26 | return UserResource 27 | } 28 | 29 | type Conversation struct { 30 | ID string 31 | 32 | CreatedBy string 33 | Participants []string 34 | MessagesCount int 35 | Active bool 36 | } 37 | 38 | // Resource interface implementation. 39 | func (c *Conversation) GetResourceName() string { 40 | return ConversationResource 41 | } 42 | 43 | type Message struct { 44 | ID string 45 | 46 | CreatedBy string 47 | CoversationId string 48 | } 49 | 50 | func (c *Message) GetResourceName() string { 51 | return MessageResource 52 | } 53 | -------------------------------------------------------------------------------- /internal/examples/basic_usage.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/el-mike/restrict/v2" 8 | "github.com/el-mike/restrict/v2/adapters" 9 | ) 10 | 11 | var basicUsagePolicy = &restrict.PolicyDefinition{ 12 | Roles: restrict.Roles{ 13 | "User": { 14 | Grants: restrict.GrantsMap{ 15 | "Conversation": { 16 | &restrict.Permission{Action: "read"}, 17 | &restrict.Permission{Action: "create"}, 18 | }, 19 | }, 20 | }, 21 | }, 22 | } 23 | 24 | func main() { 25 | // Create an instance of PolicyManager, which will be responsible for handling given PolicyDefinition. 26 | // You can use one of the built-in persistence adapters (in-memory or json/yaml file adapters), or provide your own. 27 | policyManager, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(basicUsagePolicy), true) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | manager := restrict.NewAccessManager(policyManager) 33 | 34 | if err = manager.Authorize(&restrict.AccessRequest{ 35 | Subject: &User{}, 36 | Resource: &Conversation{}, 37 | Actions: []string{"delete"}, 38 | }); err != nil { 39 | fmt.Println(err) // access denied for Action: "delete" on Resource: "Conversation" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/examples/policy_example.yaml: -------------------------------------------------------------------------------- 1 | permissionPresets: 2 | updateOwn: 3 | action: update 4 | conditions: 5 | - type: EQUAL 6 | options: 7 | name: isOwner 8 | left: 9 | source: ResourceField 10 | field: CreatedBy 11 | right: 12 | source: SubjectField 13 | field: ID 14 | roles: 15 | Admin: 16 | description: This is an Admin role, with permissions to manage Users. 17 | grants: 18 | User: 19 | - action: create 20 | parents: 21 | - User 22 | User: 23 | description: This is a simple User role, with permissions for basic chat operations. 24 | grants: 25 | Conversation: 26 | - action: read 27 | - action: create 28 | - preset: updateOwn 29 | - action: delete 30 | conditions: 31 | - type: EMPTY 32 | options: 33 | name: deleteActive 34 | value: 35 | source: ResourceField 36 | field: Active 37 | -------------------------------------------------------------------------------- /internal/examples/policy_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionPresets": { 3 | "updateOwn": { 4 | "action": "update", 5 | "conditions": [ 6 | { 7 | "type": "EQUAL", 8 | "options": { 9 | "name": "isOwner", 10 | "left": { 11 | "source": "ResourceField", 12 | "field": "CreatedBy" 13 | }, 14 | "right": { 15 | "source": "SubjectField", 16 | "field": "ID" 17 | } 18 | } 19 | } 20 | ] 21 | } 22 | }, 23 | "roles": { 24 | "Admin": { 25 | "description": "This is an Admin role, with permissions to manage Users.", 26 | "grants": { 27 | "User": [ 28 | { 29 | "action": "create" 30 | } 31 | ] 32 | }, 33 | "parents": [ 34 | "User" 35 | ] 36 | }, 37 | "User": { 38 | "description": "This is a simple User role, with permissions for basic chat operations.", 39 | "grants": { 40 | "Conversation": [ 41 | { 42 | "action": "read" 43 | }, 44 | { 45 | "action": "create" 46 | }, 47 | { 48 | "preset": "updateOwn" 49 | }, 50 | { 51 | "action": "delete", 52 | "conditions": [ 53 | { 54 | "type": "EMPTY", 55 | "options": { 56 | "name": "deleteActive", 57 | "value": { 58 | "source": "ResourceField", 59 | "field": "Active" 60 | } 61 | } 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /adapters/in_memory_adapter_test.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type inMemoryAdapterSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestInMemoryAdapterSuite(t *testing.T) { 15 | suite.Run(t, new(inMemoryAdapterSuite)) 16 | } 17 | 18 | func (s *inMemoryAdapterSuite) TestNewInMemoryAdapter() { 19 | testPolicy := getBasicPolicy() 20 | 21 | adapter := NewInMemoryAdapter(testPolicy) 22 | 23 | assert.NotNil(s.T(), adapter) 24 | assert.IsType(s.T(), new(InMemoryAdapter), adapter) 25 | } 26 | 27 | func (s *inMemoryAdapterSuite) TestLoadPolicy() { 28 | testPolicy := getBasicPolicy() 29 | 30 | adapter := NewInMemoryAdapter(testPolicy) 31 | 32 | policy, err := adapter.LoadPolicy() 33 | 34 | assert.Nil(s.T(), err) 35 | assert.NotNil(s.T(), policy) 36 | assert.Equal(s.T(), policy, testPolicy) 37 | } 38 | 39 | func (s *inMemoryAdapterSuite) TestSavePolicy() { 40 | emptyPolicy := getEmptyPolicy() 41 | testPolicy := getBasicPolicy() 42 | 43 | adapter := NewInMemoryAdapter(emptyPolicy) 44 | 45 | assert.NotNil(s.T(), adapter) 46 | 47 | policy, err := adapter.LoadPolicy() 48 | 49 | assert.Nil(s.T(), err) 50 | assert.Equal(s.T(), policy, emptyPolicy) 51 | 52 | err = adapter.SavePolicy(testPolicy) 53 | 54 | assert.Nil(s.T(), err) 55 | 56 | policy, err = adapter.LoadPolicy() 57 | 58 | assert.Nil(s.T(), err) 59 | assert.Equal(s.T(), policy, testPolicy) 60 | } 61 | -------------------------------------------------------------------------------- /permission.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | // Permission - describes an Action that can be performed in regards to 4 | // some Resource, with specified Conditions. 5 | type Permission struct { 6 | // Action that will be allowed to perform if the Permission is granted, and Conditions 7 | // are satisfied. 8 | Action string `json:"action,omitempty" yaml:"action,omitempty"` 9 | // Conditions that need to be satisfied in order to allow the subject perform given Action. 10 | Conditions Conditions `json:"conditions,omitempty" yaml:"conditions,omitempty"` 11 | // Preset allows to extend Permission defined in PolicyDefinition. 12 | Preset string `json:"preset,omitempty" yaml:"preset,omitempty"` 13 | } 14 | 15 | // Permissions - alias type for slice of Permissions. 16 | type Permissions []*Permission 17 | 18 | // mergePreset - merges preset values into Permission. 19 | func (p *Permission) mergePreset(preset *Permission) { 20 | if preset == nil { 21 | return 22 | } 23 | 24 | // Apply the action only if Permission does not specify its own. 25 | if p.Action == "" { 26 | p.Action = preset.Action 27 | } 28 | 29 | // If Permission have its own Conditions, they will be merged 30 | // together with the ones from the preset. 31 | p.Conditions = append(p.Conditions, preset.Conditions...) 32 | 33 | // We set Preset value to zero, to prevent subsequent merges while updating 34 | // policies. 35 | p.Preset = "" 36 | } 37 | 38 | // PermissionPresets - a map of reusable Permissions. Map key serves as a preset's name, 39 | // that can be later referenced by Permission. 40 | // Presets are applied when policy is loaded. 41 | type PermissionPresets map[string]*Permission 42 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // GrantsMap - alias type for map of Permission slices. 10 | type GrantsMap map[string]Permissions 11 | 12 | // Role - describes privileges of a Role's members. 13 | type Role struct { 14 | // ID - unique identifier of the Role. 15 | ID string `json:"-" yaml:"-"` 16 | // Description - optional description for a Role. 17 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 18 | // Grants - contains sets of Permissions assigned to Resources. 19 | Grants GrantsMap `json:"grants" yaml:"grants"` 20 | // Parents - other Roles that given Role inherits from. If a Permission is granted 21 | // for a parent, it is also granted for a child. 22 | Parents []string `json:"parents,omitempty" yaml:"parents,omitempty"` 23 | } 24 | 25 | // Roles - alias type for map of Roles. 26 | type Roles map[string]*Role 27 | 28 | // UnmarshalJSON - unmarshals a JSON-coded map of Roles. 29 | func (rs *Roles) UnmarshalJSON(jsonData []byte) error { 30 | *rs = Roles{} 31 | 32 | var jsonRoles map[string]*Role 33 | 34 | if err := json.Unmarshal(jsonData, &jsonRoles); err != nil { 35 | return err 36 | } 37 | 38 | for key, role := range jsonRoles { 39 | role.ID = key 40 | (*rs)[key] = role 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // UnmarshalYAML - unmarshals a YAML-coded map of Roles. 47 | func (rs *Roles) UnmarshalYAML(value *yaml.Node) error { 48 | *rs = Roles{} 49 | 50 | var yamlRoles map[string]*Role 51 | 52 | if err := value.Decode(&yamlRoles); err != nil { 53 | return err 54 | } 55 | 56 | for key, role := range yamlRoles { 57 | role.ID = key 58 | (*rs)[key] = role 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 10 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 15 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /adapters/mocks_test.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/el-mike/restrict/v2" 7 | ) 8 | 9 | const ( 10 | basicRoleName = "BasicRole" 11 | basicResourceOneName = "BasicResourceOne" 12 | ) 13 | 14 | const ( 15 | createAction = "create" 16 | readAction = "read" 17 | ) 18 | 19 | const BasicConditionOne = "BASIC_CONDITION_ONE" 20 | const BasicConditionTwo = "BASIC_CONDITION_TWO" 21 | 22 | func getBasicPolicyJSONString() string { 23 | return fmt.Sprintf(` 24 | { 25 | "roles": { 26 | "%s": { 27 | "id": "%s", 28 | "description": "Basic role", 29 | "grants": { 30 | "%s": [ 31 | { "action": "%s" }, 32 | { "action": "%s" } 33 | ] 34 | } 35 | } 36 | } 37 | }`, basicRoleName, 38 | basicRoleName, 39 | basicResourceOneName, 40 | createAction, 41 | readAction, 42 | ) 43 | } 44 | 45 | func getBasicPolicyYAMLString() string { 46 | return fmt.Sprintf(` 47 | roles: 48 | %s: 49 | id: %s 50 | description: "Basic role" 51 | grants: 52 | %s: 53 | - action: %s 54 | - action: %s 55 | `, basicRoleName, 56 | basicRoleName, 57 | basicResourceOneName, 58 | createAction, 59 | readAction, 60 | ) 61 | } 62 | 63 | func getBasicRole() *restrict.Role { 64 | return &restrict.Role{ 65 | ID: basicRoleName, 66 | Description: "Basic Role", 67 | Grants: restrict.GrantsMap{ 68 | basicResourceOneName: { 69 | &restrict.Permission{Action: createAction}, 70 | &restrict.Permission{Action: readAction}, 71 | }, 72 | }, 73 | } 74 | } 75 | 76 | func getBasicPolicy() *restrict.PolicyDefinition { 77 | return &restrict.PolicyDefinition{ 78 | Roles: restrict.Roles{ 79 | basicRoleName: getBasicRole(), 80 | }, 81 | } 82 | } 83 | 84 | func getEmptyPolicy() *restrict.PolicyDefinition { 85 | return &restrict.PolicyDefinition{} 86 | } 87 | -------------------------------------------------------------------------------- /internal/tests/custom_condition_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/el-mike/restrict/v2" 7 | ) 8 | 9 | const hasUserConditionType = "BELONGS_TO" 10 | 11 | type hasUserCondition struct{} 12 | 13 | func (c *hasUserCondition) Type() string { 14 | return hasUserConditionType 15 | } 16 | 17 | func (c *hasUserCondition) Check(request *restrict.AccessRequest) error { 18 | user, ok := request.Subject.(*User) 19 | if !ok { 20 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Subject has to be a User")) 21 | } 22 | 23 | conversation, ok := request.Resource.(*Conversation) 24 | if !ok { 25 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Resource has to be a Conversation")) 26 | } 27 | 28 | for _, userId := range conversation.Participants { 29 | if userId == user.ID { 30 | return nil 31 | } 32 | } 33 | 34 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("User does not belong to Conversation with ID: %s", conversation.ID)) 35 | } 36 | 37 | const greatherThanType = "GREATER_THAN" 38 | 39 | type greaterThanCondition struct { 40 | Value *restrict.ValueDescriptor `json:"value" yaml:"value"` 41 | } 42 | 43 | func (c *greaterThanCondition) Type() string { 44 | return greatherThanType 45 | } 46 | 47 | func (c *greaterThanCondition) Check(request *restrict.AccessRequest) error { 48 | value, err := c.Value.GetValue(request) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | intValue, ok := value.(int) 54 | if !ok { 55 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Value has to be an integer")) 56 | } 57 | 58 | intMax, ok := request.Context["Max"].(int) 59 | if !ok { 60 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Max has to be an integer")) 61 | } 62 | 63 | if intValue > intMax { 64 | return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Value is greater than max")) 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /value_descriptor.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/el-mike/restrict/v2/internal/utils" 7 | ) 8 | 9 | // ValueDescriptor - describes a value that will be tested in its parent Condition. 10 | type ValueDescriptor struct { 11 | // Source - source of the value, one of the predefined enum type (ValueSource). 12 | Source ValueSource `json:"source,omitempty" yaml:"source,omitempty"` 13 | // Field - field on the given ValueSource that should hold the value. 14 | Field string `json:"field,omitempty" yaml:"field,omitempty"` 15 | // Value - explicit value taken when using ValueSource.Explicit as value source. 16 | Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` 17 | } 18 | 19 | // GetValue - returns real value represented by given ValueDescriptor. 20 | func (vd *ValueDescriptor) GetValue(request *AccessRequest) (interface{}, error) { 21 | if vd == nil { 22 | return nil, newValueDescriptorMalformedError(vd, fmt.Errorf("ValueDescriptor cannot be nil")) 23 | } 24 | 25 | if vd.Source == Explicit { 26 | return vd.Value, nil 27 | } 28 | 29 | if vd.Field == "" { 30 | return nil, newValueDescriptorMalformedError(vd, fmt.Errorf("Field cannot be empty for Source: \"%s\"", vd.Source.String())) 31 | } 32 | 33 | var source interface{} 34 | 35 | if vd.Source == SubjectField { 36 | source = request.Subject 37 | } 38 | 39 | if vd.Source == ResourceField { 40 | source = request.Resource 41 | } 42 | 43 | if vd.Source == ContextField { 44 | source = request.Context 45 | } 46 | 47 | if source == nil { 48 | return nil, newValueDescriptorMalformedError(vd, fmt.Errorf("Source could not be find")) 49 | } 50 | 51 | if utils.IsMap(source) { 52 | return utils.GetMapValue(source, vd.Field), nil 53 | } 54 | 55 | if utils.HasField(source, vd.Field) { 56 | return utils.GetStructFieldValue(source, vd.Field), nil 57 | } 58 | 59 | return nil, newValueDescriptorMalformedError(vd, fmt.Errorf("Field \"%s\" does not exist on Source: \"%s\"", vd.Field, vd.Source.String())) 60 | } 61 | -------------------------------------------------------------------------------- /internal/utils/type_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // IsSameType - returns true if both arguments are the same type, false otherwise. 8 | func IsSameType(a, b interface{}) bool { 9 | return reflect.TypeOf(a) == reflect.TypeOf(b) 10 | } 11 | 12 | // IsStruct - returns true if argument is a struct, false otherwise. 13 | func IsStruct(value interface{}) bool { 14 | return reflect.ValueOf(value).Kind() == reflect.Struct 15 | } 16 | 17 | // IsMap - returns true if argument is a Map, false otherwise. 18 | func IsMap(value interface{}) bool { 19 | return reflect.ValueOf(value).Kind() == reflect.Map 20 | } 21 | 22 | // GetMapValue - returns a value under given key in passed map. 23 | func GetMapValue(mapValue interface{}, keyValue interface{}) interface{} { 24 | rMapValue := reflect.ValueOf(mapValue) 25 | 26 | if rMapValue.Kind() == reflect.Ptr { 27 | rMapValue = rMapValue.Elem() 28 | } 29 | 30 | if rMapValue.Kind() != reflect.Map { 31 | return nil 32 | } 33 | 34 | value := rMapValue.MapIndex(reflect.ValueOf(keyValue)) 35 | 36 | if !value.IsValid() { 37 | return nil 38 | } 39 | 40 | return value.Interface() 41 | } 42 | 43 | // HasField - returns true if given field exists on passed struct, false otherwise. 44 | func HasField(value interface{}, fieldName string) bool { 45 | rValue := reflect.ValueOf(value) 46 | 47 | if rValue.Kind() == reflect.Ptr { 48 | rValue = rValue.Elem() 49 | } 50 | 51 | if rValue.Kind() != reflect.Struct { 52 | return false 53 | } 54 | 55 | return rValue.FieldByName(fieldName).IsValid() 56 | } 57 | 58 | // GetStructFieldValue - returns a value under given field in passed struct. 59 | func GetStructFieldValue(structValue interface{}, fieldName string) interface{} { 60 | rStructValue := reflect.ValueOf(structValue) 61 | 62 | if rStructValue.Kind() == reflect.Ptr { 63 | rStructValue = rStructValue.Elem() 64 | } 65 | 66 | if rStructValue.Kind() != reflect.Struct { 67 | return nil 68 | } 69 | 70 | rValue := rStructValue.FieldByName(fieldName) 71 | 72 | if !rValue.IsValid() || !rValue.CanInterface() { 73 | return nil 74 | } 75 | 76 | return rValue.Interface() 77 | } 78 | -------------------------------------------------------------------------------- /adapters/file_adapter_deps.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type FilePerm os.FileMode 11 | 12 | // FileReadWriter - facade interface for os read/write file functions. 13 | type FileReadWriter interface { 14 | ReadFile(name string) ([]byte, error) 15 | WriteFile(name string, data []byte, perm FilePerm) error 16 | } 17 | 18 | // defaultFileHandler - fileReadWriter implementation. 19 | type defaultFileHandler struct{} 20 | 21 | func newDefaultFileHandler() *defaultFileHandler { 22 | return &defaultFileHandler{} 23 | } 24 | 25 | func (dh *defaultFileHandler) ReadFile(name string) ([]byte, error) { 26 | return os.ReadFile(name) 27 | } 28 | 29 | func (dh *defaultFileHandler) WriteFile(name string, data []byte, perm FilePerm) error { 30 | return os.WriteFile(name, data, os.FileMode(perm)) 31 | } 32 | 33 | // JSONMarshalUnmarshaler - facade interface for json operations. 34 | type JSONMarshalUnmarshaler interface { 35 | Unmarshal(data []byte, v interface{}) error 36 | MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) 37 | } 38 | 39 | type defaultJSONHandler struct{} 40 | 41 | func newDefaultJSONHandler() *defaultJSONHandler { 42 | return &defaultJSONHandler{} 43 | } 44 | 45 | func (dh *defaultJSONHandler) Unmarshal(data []byte, v interface{}) error { 46 | return json.Unmarshal(data, v) 47 | } 48 | 49 | func (dh *defaultJSONHandler) MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { 50 | return json.MarshalIndent(v, prefix, indent) 51 | } 52 | 53 | // YAMLMarshalUnmarshaler - facade interface for yaml operations. 54 | type YAMLMarshalUnmarshaler interface { 55 | Unmarshal(in []byte, out interface{}) (err error) 56 | Marshal(in interface{}) (out []byte, err error) 57 | } 58 | 59 | type defaultYAMLHandler struct{} 60 | 61 | func newDefaultYAMLHandler() *defaultYAMLHandler { 62 | return &defaultYAMLHandler{} 63 | } 64 | 65 | func (dh *defaultYAMLHandler) Unmarshal(in []byte, out interface{}) error { 66 | return yaml.Unmarshal(in, out) 67 | } 68 | 69 | func (dh *defaultYAMLHandler) Marshal(in interface{}) ([]byte, error) { 70 | return yaml.Marshal(in) 71 | } 72 | -------------------------------------------------------------------------------- /permission_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type permissionSuite struct { 11 | suite.Suite 12 | 13 | testPresetName string 14 | testAction string 15 | } 16 | 17 | func TestPermissionSuite(t *testing.T) { 18 | suite.Run(t, new(permissionSuite)) 19 | } 20 | 21 | func (s *permissionSuite) SetupSuite() { 22 | s.testAction = "test-action" 23 | s.testPresetName = "testPreset" 24 | } 25 | 26 | func (s *permissionSuite) TestMergePreset() { 27 | // Do not extend conditions 28 | testCondition := new(conditionMock) 29 | 30 | testPreset := &Permission{ 31 | Action: s.testAction, 32 | Conditions: Conditions{ 33 | testCondition, 34 | testCondition, 35 | }, 36 | } 37 | 38 | testPermission := &Permission{} 39 | 40 | assert.NotPanics(s.T(), func() { 41 | testPermission.mergePreset(nil) 42 | }) 43 | 44 | testPermission.mergePreset(testPreset) 45 | 46 | assert.Equal(s.T(), testPreset.Action, testPermission.Action) 47 | assert.Equal(s.T(), "", testPermission.Preset) 48 | assert.Equal(s.T(), 2, len(testPermission.Conditions)) 49 | 50 | assert.Equal(s.T(), testCondition, testPermission.Conditions[0]) 51 | 52 | // Extend conditions 53 | testPermission = &Permission{ 54 | Conditions: Conditions{ 55 | testCondition, 56 | }, 57 | } 58 | 59 | testPermission.mergePreset(testPreset) 60 | 61 | assert.Equal(s.T(), 3, len(testPermission.Conditions)) 62 | 63 | // Should not override Permission's own action. 64 | customAction := "customAction" 65 | testPermission = &Permission{ 66 | Action: customAction, 67 | } 68 | 69 | testPermission.mergePreset(testPreset) 70 | 71 | assert.Equal(s.T(), customAction, testPermission.Action) 72 | } 73 | 74 | func (s *permissionSuite) TestMergePreset_NilPresetConditions() { 75 | testPreset := &Permission{ 76 | Action: s.testAction, 77 | } 78 | 79 | testPermission := &Permission{} 80 | 81 | assert.NotPanics(s.T(), func() { 82 | testPermission.mergePreset(nil) 83 | }) 84 | 85 | testPermission.mergePreset(testPreset) 86 | assert.Equal(s.T(), 0, len(testPermission.Conditions)) 87 | } 88 | -------------------------------------------------------------------------------- /condition_empty.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | const ( 9 | // EmptyConditionType - EmptyCondition's type identifier. 10 | EmptyConditionType = "EMPTY" 11 | // NotEmptyConditionType - NotEmptyCondition's type identifier. 12 | NotEmptyConditionType = "NOT_EMPTY" 13 | ) 14 | 15 | // baseEmptyCondition - describes fields needed by Empty/NotEmpty Conditions. 16 | type baseEmptyCondition struct { 17 | // ID - Condition's id, useful when there is a need to identify failing Condition. 18 | ID string `json:"name,omitempty" yaml:"name,omitempty"` 19 | // Value - ValueDescriptor for the value being checked. 20 | Value *ValueDescriptor `json:"value" yaml:"value"` 21 | } 22 | 23 | // EmptyCondition - Condition for testing whether given value is empty. 24 | type EmptyCondition baseEmptyCondition 25 | 26 | // Type - returns Condition's type. 27 | func (c *EmptyCondition) Type() string { 28 | return EmptyConditionType 29 | } 30 | 31 | // Check - returns true if value is empty (zero-like), false otherwise. 32 | func (c *EmptyCondition) Check(request *AccessRequest) error { 33 | value, err := c.Value.GetValue(request) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if value == nil { 39 | return nil 40 | } 41 | 42 | empty := reflect.ValueOf(value).IsZero() 43 | 44 | if !empty { 45 | return NewConditionNotSatisfiedError(c, request, fmt.Errorf("value \"%v\" is not empty", value)) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | type NotEmptyCondition baseEmptyCondition 52 | 53 | // Type - returns Condition's type. 54 | func (c *NotEmptyCondition) Type() string { 55 | return NotEmptyConditionType 56 | } 57 | 58 | // Check - returns true if value is not empty (zero-like), false otherwise. 59 | func (c *NotEmptyCondition) Check(request *AccessRequest) error { 60 | value, err := c.Value.GetValue(request) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if value == nil { 66 | return NewConditionNotSatisfiedError(c, request, fmt.Errorf("value \"%v\" is empty", value)) 67 | } 68 | 69 | empty := reflect.ValueOf(value).IsZero() 70 | 71 | if empty { 72 | return NewConditionNotSatisfiedError(c, request, fmt.Errorf("value \"%v\" is empty", value)) 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /value_source.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // ValueSource - enum type for source of value for given ValueDescriptor. 11 | type ValueSource int 12 | 13 | const ( 14 | // NoopValueSource - zero value for ValueSource, useful when marshaling/unmarshaling. 15 | noopValueSource ValueSource = iota 16 | // SubjectField - value that comes from Subject's field. 17 | SubjectField 18 | // ResourceField - value that comes from Resource's field. 19 | ResourceField 20 | // ContextField - value that comes from Context's field. 21 | ContextField 22 | // Explicit - value set explicitly in PolicyDefinition. 23 | Explicit 24 | ) 25 | 26 | var byValue = map[ValueSource]string{ 27 | SubjectField: "SubjectField", 28 | ResourceField: "ResourceField", 29 | ContextField: "ContextField", 30 | Explicit: "Explicit", 31 | } 32 | 33 | var byName = map[string]ValueSource{ 34 | "SubjectField": SubjectField, 35 | "ResourceField": ResourceField, 36 | "ContextField": ContextField, 37 | "Explicit": Explicit, 38 | } 39 | 40 | // String - Stringer implementation. 41 | func (vs ValueSource) String() string { 42 | return byValue[vs] 43 | } 44 | 45 | // MarshalJSON - marshals a ValueSource enum into its name as string. 46 | func (vs ValueSource) MarshalJSON() ([]byte, error) { 47 | buffer := bytes.NewBufferString(`"`) 48 | 49 | buffer.WriteString(byValue[vs]) 50 | buffer.WriteString(`"`) 51 | 52 | return buffer.Bytes(), nil 53 | } 54 | 55 | // MarshalYAML - marshals a ValueSource enum into its name as string. 56 | func (vs ValueSource) MarshalYAML() (interface{}, error) { 57 | return byValue[vs], nil 58 | } 59 | 60 | // UnmarshalJSON - unmarshals a string into ValueSource. 61 | func (vs *ValueSource) UnmarshalJSON(jsonData []byte) error { 62 | var sourceName string 63 | 64 | if err := json.Unmarshal(jsonData, &sourceName); err != nil { 65 | return err 66 | } 67 | 68 | *vs = byName[sourceName] 69 | 70 | return nil 71 | } 72 | 73 | // UnmarshalYAML - unmarshals a string into ValueSource. 74 | func (vs *ValueSource) UnmarshalYAML(value *yaml.Node) error { 75 | var sourceName string 76 | 77 | if err := value.Decode(&sourceName); err != nil { 78 | return err 79 | } 80 | 81 | *vs = byName[sourceName] 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /condition_equal.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | const ( 9 | // EqualConditionType - EqualCondition's type identifier. 10 | EqualConditionType = "EQUAL" 11 | //NotEqualConditionType - NotEqualCondition's type identifier. 12 | NotEqualConditionType = "NOT_EQUAL" 13 | ) 14 | 15 | // baseEqualCondition - describes fields needed by Equal/NotEqual Conditions. 16 | type baseEqualCondition struct { 17 | // ID - Condition's id, useful when there is a need to identify failing Condition. 18 | ID string `json:"name,omitempty" yaml:"name,omitempty"` 19 | // Left - ValueDescriptor for left operand of equality check. 20 | Left *ValueDescriptor `json:"left" yaml:"left"` 21 | // Right - ValueDescriptor for right operand of equality check. 22 | Right *ValueDescriptor `json:"right" yaml:"right"` 23 | } 24 | 25 | // EqualCondition - checks whether given value (Left) is equal to some other value (Right). 26 | type EqualCondition baseEqualCondition 27 | 28 | // Type - returns Condition's type. 29 | func (c *EqualCondition) Type() string { 30 | return EqualConditionType 31 | } 32 | 33 | // Check - returns true if values are equal, false otherwise. 34 | func (c *EqualCondition) Check(request *AccessRequest) error { 35 | left, right, err := unpackEqualDescriptors(c.Left, c.Right, request) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if !reflect.DeepEqual(left, right) { 41 | return NewConditionNotSatisfiedError(c, request, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right)) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // NotEqualCondition - checks whether given value (Left) is not equal to some other value (Right). 48 | type NotEqualCondition baseEqualCondition 49 | 50 | // Type - returns Condition's type. 51 | func (c *NotEqualCondition) Type() string { 52 | return NotEqualConditionType 53 | } 54 | 55 | // Check - returns true if values are not equal, false otherwise. 56 | func (c *NotEqualCondition) Check(request *AccessRequest) error { 57 | left, right, err := unpackEqualDescriptors(c.Left, c.Right, request) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if reflect.DeepEqual(left, right) { 63 | return NewConditionNotSatisfiedError(c, request, fmt.Errorf("values \"%v\" and \"%v\" are equal", left, right)) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // unpackDescriptors - helper function for unpacking ValueDescriptors' values. 70 | func unpackEqualDescriptors(left, right *ValueDescriptor, request *AccessRequest) (interface{}, interface{}, error) { 71 | leftValue, err := left.GetValue(request) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | rightValue, err := right.GetValue(request) 77 | if err != nil { 78 | return nil, nil, err 79 | } 80 | 81 | return leftValue, rightValue, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/examples/error_handling_example.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/el-mike/restrict/v2" 8 | "github.com/el-mike/restrict/v2/adapters" 9 | ) 10 | 11 | var errorHandlingExamplePolicy = &restrict.PolicyDefinition{ 12 | Roles: restrict.Roles{ 13 | "User": { 14 | Grants: restrict.GrantsMap{ 15 | "Conversation": { 16 | &restrict.Permission{Action: "read"}, 17 | &restrict.Permission{Action: "create"}, 18 | }, 19 | }, 20 | }, 21 | }, 22 | } 23 | 24 | func mainErrorHandling() { 25 | // Create an instance of PolicyManager, which will be responsible for handling given PolicyDefinition. 26 | // You can use one of the built-in persistence adapters (in-memory or json/yaml file adapters), or provide your own. 27 | policyManager, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(errorHandlingExamplePolicy), true) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | manager := restrict.NewAccessManager(policyManager) 33 | err = manager.Authorize(&restrict.AccessRequest{ 34 | Subject: &User{}, 35 | Resource: &Conversation{}, 36 | Actions: []string{"read"}, 37 | }) 38 | 39 | if accessError, ok := err.(*restrict.AccessDeniedError); ok { 40 | // Error() implementation. Returns a message in a form: "access denied for Action/s: ... on Resource: ..." 41 | fmt.Println(accessError) 42 | // Returns an AccessRequest that failed. 43 | fmt.Println(accessError.Request) 44 | // Returns first reason for the denied access. 45 | // Especially helpful in fail-early mode, where there will only be one Reason. 46 | fmt.Println(accessError.FirstReason()) 47 | 48 | // Reasons property will hold all errors that caused the access to be denied. 49 | for _, permissionErr := range accessError.Reasons { 50 | fmt.Println(permissionErr) 51 | fmt.Println(permissionErr.Action) 52 | fmt.Println(permissionErr.RoleName) 53 | fmt.Println(permissionErr.ResourceName) 54 | 55 | // Returns first ConditionNotSatisfied error for given PermissionError, if any was returned for given PermissionError. 56 | // Especially helpful in fail-early mode, where there will only be one failed Condition. 57 | fmt.Println(permissionErr.FirstConditionError()) 58 | 59 | // ConditionErrors property will hold all ConditionNotSatisfied errors. 60 | for _, conditionErr := range permissionErr.ConditionErrors { 61 | fmt.Println(conditionErr) 62 | fmt.Println(conditionErr.Reason) 63 | 64 | // Every ConditionNotSatisfied contains an instance of Condition that returned it, 65 | // so it can be tested using type assertion to get more details about failed Condition. 66 | if emptyCondition, ok := conditionErr.Condition.(*restrict.EmptyCondition); ok { 67 | fmt.Println(emptyCondition.ID) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/tests/policies_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import "github.com/el-mike/restrict/v2" 4 | 5 | var PolicyOne = &restrict.PolicyDefinition{ 6 | PermissionPresets: restrict.PermissionPresets{ 7 | "updateOwn": &restrict.Permission{ 8 | Action: "update", 9 | Conditions: restrict.Conditions{ 10 | &restrict.EqualCondition{ 11 | ID: "isOwner", 12 | Left: &restrict.ValueDescriptor{ 13 | Source: restrict.ResourceField, 14 | Field: "CreatedBy", 15 | }, 16 | Right: &restrict.ValueDescriptor{ 17 | Source: restrict.SubjectField, 18 | Field: "ID", 19 | }, 20 | }, 21 | }, 22 | }, 23 | "readWhereBelongs": &restrict.Permission{ 24 | Action: "read", 25 | Conditions: restrict.Conditions{ 26 | &hasUserCondition{}, 27 | }, 28 | }, 29 | "accessSelf": &restrict.Permission{ 30 | Conditions: restrict.Conditions{ 31 | &restrict.EqualCondition{ 32 | ID: "self", 33 | Left: &restrict.ValueDescriptor{ 34 | Source: restrict.ResourceField, 35 | Field: "ID", 36 | }, 37 | Right: &restrict.ValueDescriptor{ 38 | Source: restrict.SubjectField, 39 | Field: "ID", 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | Roles: restrict.Roles{ 46 | BasicUserRole: { 47 | Grants: restrict.GrantsMap{ 48 | UserResource: { 49 | &restrict.Permission{ 50 | Action: "read", 51 | Preset: "accessSelf", 52 | }, 53 | }, 54 | }, 55 | }, 56 | UserRole: { 57 | Parents: []string{BasicUserRole}, 58 | Grants: restrict.GrantsMap{ 59 | ConversationResource: { 60 | &restrict.Permission{Preset: "readWhereBelongs"}, 61 | &restrict.Permission{Preset: "updateOwn"}, 62 | &restrict.Permission{Action: "create"}, 63 | &restrict.Permission{ 64 | Action: "delete", 65 | Conditions: restrict.Conditions{ 66 | &restrict.EmptyCondition{ 67 | ID: "deleteActive", 68 | Value: &restrict.ValueDescriptor{ 69 | Source: restrict.ResourceField, 70 | Field: "Active", 71 | }, 72 | }, 73 | &greaterThanCondition{ 74 | Value: &restrict.ValueDescriptor{ 75 | Source: restrict.ResourceField, 76 | Field: "MessagesCount", 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | AdminRole: { 85 | Parents: []string{UserRole}, 86 | Grants: restrict.GrantsMap{ 87 | ConversationResource: { 88 | &restrict.Permission{Action: "read"}, 89 | }, 90 | UserResource: { 91 | &restrict.Permission{Action: "create"}, 92 | &restrict.Permission{Action: "read"}, 93 | &restrict.Permission{Action: "update"}, 94 | &restrict.Permission{Action: "delete"}, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /internal/tests/test_policy.yaml: -------------------------------------------------------------------------------- 1 | permissionPresets: 2 | accessSelf: 3 | conditions: 4 | - type: EQUAL 5 | options: 6 | name: self 7 | left: 8 | source: ResourceField 9 | field: ID 10 | right: 11 | source: SubjectField 12 | field: ID 13 | readWhereBelongs: 14 | action: read 15 | conditions: 16 | - type: BELONGS_TO 17 | options: {} 18 | updateOwn: 19 | action: update 20 | conditions: 21 | - type: EQUAL 22 | options: 23 | name: isOwner 24 | left: 25 | source: ResourceField 26 | field: CreatedBy 27 | right: 28 | source: SubjectField 29 | field: ID 30 | roles: 31 | Admin: 32 | grants: 33 | Conversation: 34 | - action: read 35 | User: 36 | - action: create 37 | - action: read 38 | - action: update 39 | - action: delete 40 | parents: 41 | - User 42 | BasicUser: 43 | grants: 44 | User: 45 | - action: read 46 | conditions: 47 | - type: EQUAL 48 | options: 49 | name: self 50 | left: 51 | source: ResourceField 52 | field: ID 53 | right: 54 | source: SubjectField 55 | field: ID 56 | User: 57 | grants: 58 | Conversation: 59 | - action: read 60 | conditions: 61 | - type: BELONGS_TO 62 | options: {} 63 | - action: update 64 | conditions: 65 | - type: EQUAL 66 | options: 67 | name: isOwner 68 | left: 69 | source: ResourceField 70 | field: CreatedBy 71 | right: 72 | source: SubjectField 73 | field: ID 74 | - action: create 75 | - action: delete 76 | conditions: 77 | - type: EMPTY 78 | options: 79 | name: deleteActive 80 | value: 81 | source: ResourceField 82 | field: Active 83 | - type: GREATER_THAN 84 | options: 85 | value: 86 | source: ResourceField 87 | field: MessagesCount 88 | parents: 89 | - BasicUser 90 | -------------------------------------------------------------------------------- /role_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type roleSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func TestRoleSuite(t *testing.T) { 18 | suite.Run(t, new(roleSuite)) 19 | } 20 | 21 | func (s *roleSuite) TestUnmarshalJSON() { 22 | testRoleOne := "TestRole1" 23 | testRoleTwo := "TestRole2" 24 | 25 | rolesData := []byte(fmt.Sprintf(`{ 26 | "%s": { 27 | "grants": { 28 | "%s": [ 29 | {"action": "create"} 30 | ] 31 | } 32 | }, 33 | "%s": { 34 | "grants": { 35 | "%s": [ 36 | { "action": "update" } 37 | ] 38 | } 39 | } 40 | 41 | }`, testRoleOne, basicResourceOneName, testRoleTwo, basicResourceOneName)) 42 | 43 | assert.True(s.T(), json.Valid(rolesData)) 44 | 45 | testRoles := Roles{} 46 | 47 | err := json.Unmarshal(rolesData, &testRoles) 48 | 49 | assert.Nil(s.T(), err) 50 | 51 | assert.IsType(s.T(), new(Role), testRoles[testRoleOne]) 52 | assert.IsType(s.T(), new(Role), testRoles[testRoleTwo]) 53 | 54 | assert.Equal(s.T(), testRoleOne, testRoles[testRoleOne].ID) 55 | assert.Equal(s.T(), testRoleTwo, testRoles[testRoleTwo].ID) 56 | } 57 | 58 | func (s *roleSuite) TestUnmarshalJSON_InvalidData() { 59 | // Array instead of map for grants 60 | rolesData := []byte(`{ 61 | "TestRole1": { 62 | "grants": [ 63 | {"action": "create"}, 64 | {"action": "update"} 65 | ] 66 | } 67 | }`) 68 | 69 | testRoles := Roles{} 70 | 71 | err := json.Unmarshal(rolesData, &testRoles) 72 | 73 | assert.Error(s.T(), err) 74 | assert.NotPanics(s.T(), func() { json.Unmarshal(rolesData, &testRoles) }) // nolint 75 | } 76 | 77 | func (s *roleSuite) TestUnmarshalYAML() { 78 | testRoleOne := "TestRole1" 79 | testRoleTwo := "TestRole2" 80 | 81 | rolesData := []byte(fmt.Sprintf(` 82 | %s: 83 | grants: 84 | %s: 85 | - action: create 86 | %s: 87 | grants: 88 | %s: 89 | - action: update 90 | `, testRoleOne, basicResourceOneName, testRoleTwo, basicResourceOneName)) 91 | 92 | testRoles := Roles{} 93 | 94 | err := yaml.Unmarshal(rolesData, &testRoles) 95 | 96 | assert.Nil(s.T(), err) 97 | 98 | assert.IsType(s.T(), new(Role), testRoles[testRoleOne]) 99 | assert.IsType(s.T(), new(Role), testRoles[testRoleTwo]) 100 | 101 | assert.Equal(s.T(), testRoleOne, testRoles[testRoleOne].ID) 102 | assert.Equal(s.T(), testRoleTwo, testRoles[testRoleTwo].ID) 103 | } 104 | 105 | func (s *roleSuite) TestUnmarshalYAML_InvalidData() { 106 | // Array instead of map for grants 107 | rolesData := []byte(` 108 | TestRole1: 109 | grants: 110 | - action: create 111 | - action: update 112 | `) 113 | 114 | testRoles := Roles{} 115 | 116 | err := yaml.Unmarshal(rolesData, &testRoles) 117 | 118 | assert.Error(s.T(), err) 119 | assert.NotPanics(s.T(), func() { yaml.Unmarshal(rolesData, &testRoles) }) // nolint 120 | } 121 | -------------------------------------------------------------------------------- /internal/utils/type_utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type testStruct struct { 11 | IntField int 12 | StringField string 13 | privateIntField int //nolint 14 | } 15 | 16 | type typeUtilsSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func TestTypeUtilsSuite(t *testing.T) { 21 | suite.Run(t, new(typeUtilsSuite)) 22 | } 23 | 24 | func (s *typeUtilsSuite) TestIsSameType() { 25 | assert.True(s.T(), IsSameType(1, 2)) 26 | assert.False(s.T(), IsSameType(1, "")) 27 | assert.False(s.T(), IsSameType(nil, "")) 28 | 29 | aStruct := &testStruct{} 30 | bStruct := &testStruct{ 31 | IntField: 1, 32 | } 33 | 34 | assert.True(s.T(), IsSameType(aStruct, bStruct)) 35 | assert.False(s.T(), IsSameType(aStruct, &bStruct)) 36 | } 37 | 38 | func (s *typeUtilsSuite) TestIsStruct() { 39 | assert.False(s.T(), IsStruct(1)) 40 | assert.False(s.T(), IsStruct(nil)) 41 | 42 | testStruct := testStruct{} 43 | 44 | assert.True(s.T(), IsStruct(testStruct)) 45 | assert.False(s.T(), IsStruct(&testStruct)) 46 | 47 | testMap := map[string]int{} 48 | 49 | assert.False(s.T(), IsStruct(testMap)) 50 | } 51 | 52 | func (s *typeUtilsSuite) TestIsMap() { 53 | assert.False(s.T(), IsMap(1)) 54 | assert.False(s.T(), IsMap(nil)) 55 | 56 | testStruct := testStruct{} 57 | 58 | assert.False(s.T(), IsMap(testStruct)) 59 | assert.False(s.T(), IsMap(&testStruct)) 60 | 61 | testMap := map[string]int{} 62 | 63 | assert.True(s.T(), IsMap(testMap)) 64 | assert.False(s.T(), IsMap(&testMap)) 65 | } 66 | 67 | func (s *typeUtilsSuite) TestGetMapValue() { 68 | testKey := "testKey" 69 | testValue := 1 70 | 71 | assert.Nil(s.T(), GetMapValue(1, testKey)) 72 | 73 | testMap := map[string]int{ 74 | "testKey": testValue, 75 | } 76 | 77 | assert.Equal(s.T(), testValue, GetMapValue(testMap, testKey)) 78 | assert.Equal(s.T(), testValue, GetMapValue(&testMap, testKey)) 79 | assert.Zero(s.T(), 0, GetMapValue(testMap, "invalidKey")) 80 | } 81 | 82 | func (s *typeUtilsSuite) TestHasField() { 83 | testStruct := testStruct{ 84 | IntField: 1, 85 | StringField: "test", 86 | } 87 | 88 | assert.False(s.T(), HasField(1, "test")) 89 | assert.True(s.T(), HasField(testStruct, "IntField")) 90 | assert.True(s.T(), HasField(testStruct, "StringField")) 91 | assert.True(s.T(), HasField(&testStruct, "IntField")) 92 | assert.False(s.T(), HasField(testStruct, "InvalidField")) 93 | } 94 | 95 | func (s *typeUtilsSuite) TestGetStructFieldValue() { 96 | testInt := 1 97 | testString := "test" 98 | 99 | testStruct := testStruct{ 100 | IntField: testInt, 101 | StringField: testString, 102 | } 103 | 104 | assert.Equal(s.T(), testInt, GetStructFieldValue(testStruct, "IntField")) 105 | assert.Equal(s.T(), testString, GetStructFieldValue(testStruct, "StringField")) 106 | 107 | assert.Equal(s.T(), testInt, GetStructFieldValue(&testStruct, "IntField")) 108 | 109 | assert.Nil(s.T(), GetStructFieldValue(1, "IntField")) 110 | assert.Nil(s.T(), GetStructFieldValue(testStruct, "InvalidKey")) 111 | assert.Nil(s.T(), GetStructFieldValue(testStruct, "privateIntField")) 112 | } 113 | -------------------------------------------------------------------------------- /internal/examples/policy_example.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/el-mike/restrict/v2" 4 | 5 | var ExamplePolicy = &restrict.PolicyDefinition{ 6 | // A map of reusable Permissions. Key corresponds to a preset's name, which can 7 | // be later used to apply it. 8 | PermissionPresets: restrict.PermissionPresets{ 9 | "updateOwn": &restrict.Permission{ 10 | // An action that given Permission allows to perform. 11 | Action: "update", 12 | // Optional Conditions that when defined, need to be satisfied in order 13 | // to allow the access. 14 | Conditions: restrict.Conditions{ 15 | // EqualCondition requires two values (described by ValueDescriptors) 16 | // to be equal in order to grant the access. 17 | // In this example we want to check if Conversation.CreatedBy and User.ID 18 | // are the same, meaning that Conversation was created by given User. 19 | &restrict.EqualCondition{ 20 | ID: "isOwner", 21 | Left: &restrict.ValueDescriptor{ 22 | Source: restrict.ResourceField, 23 | Field: "CreatedBy", 24 | }, 25 | Right: &restrict.ValueDescriptor{ 26 | Source: restrict.SubjectField, 27 | Field: "ID", 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | // A map of Roles. Key corresponds to a Role that any Subject in your system can belong to. 34 | Roles: restrict.Roles{ 35 | "User": { 36 | // Optional, human readable description. 37 | Description: "This is a simple User role, with permissions for basic chat operations.", 38 | // Map of Permissions per Resource. 39 | // Grants map can be nil, meaning given Role has no Permissions (but can still inherit some). 40 | Grants: restrict.GrantsMap{ 41 | "Conversation": { 42 | // Subject "User" can "read" any "Conversation". 43 | &restrict.Permission{Action: "read"}, 44 | // Subject "User" can "create" a "Conversation". 45 | &restrict.Permission{Action: "create"}, 46 | // Subject "User" can "update" ONLY a "Coversation" that was 47 | // created by it. Check "updateOwn" preset definition above. 48 | &restrict.Permission{Preset: "updateOwn"}, 49 | // Subject "User" can "delete" ONLY inactive "Conversation". 50 | &restrict.Permission{ 51 | Action: "delete", 52 | Conditions: restrict.Conditions{ 53 | // EmptyCondition requires a value (described by ValueDescriptor) 54 | // to be empty (falsy) in order to grant the access. 55 | // In this example, we want Conversation.Active to be false. 56 | &restrict.EmptyCondition{ 57 | ID: "deleteActive", 58 | Value: &restrict.ValueDescriptor{ 59 | Source: restrict.ResourceField, 60 | Field: "Active", 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | "Admin": { 69 | Description: "This is an Admin role, with permissions to manage Users.", 70 | // "Admin" can do everything "User" can. 71 | Parents: []string{"User"}, 72 | // AND can also perform other operations that User itself 73 | // is not allowed to do. 74 | Grants: restrict.GrantsMap{ 75 | // Please note that in order to make this work, 76 | // User needs to implement Resource interface. 77 | "User": { 78 | // Subject "Admin" can create a "User". 79 | &restrict.Permission{Action: "create"}, 80 | }, 81 | }, 82 | }, 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /mocks_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | const ( 8 | basicRoleOneName = "BasicRoleOne" 9 | basicRoleTwoName = "BasicRoleTwo" 10 | basicParentRoleName = "BasicParentRole" 11 | basicResourceOneName = "BasicResourceOne" 12 | basicResourceTwoName = "BasicResourceTwo" 13 | ) 14 | 15 | const ( 16 | createAction = "create" 17 | readAction = "read" 18 | updateAction = "update" 19 | deleteAction = "delete" 20 | ) 21 | 22 | const basicConditionOne = "BASIC_CONDITION_ONE" 23 | 24 | func getBasicRolesSet() []string { 25 | return []string{basicRoleOneName} 26 | } 27 | 28 | type subjectMock struct { 29 | mock.Mock 30 | 31 | ID string 32 | FieldOne string 33 | FieldTwo int 34 | FieldThree []int 35 | } 36 | 37 | func (m *subjectMock) GetRoles() []string { 38 | args := m.Called() 39 | 40 | // If not specified exactly when using the mock, it will return an array with single role. 41 | // This is helpful for tests where roles don't matter than much. 42 | if args.Get(0) == nil { 43 | return getBasicRolesSet() 44 | } 45 | 46 | return args.Get(0).([]string) 47 | } 48 | 49 | type resourceMock struct { 50 | mock.Mock 51 | 52 | ID string 53 | CreatedBy string 54 | Type string 55 | FieldOne string 56 | FieldTwo int 57 | FieldThree []int 58 | } 59 | 60 | func (m *resourceMock) GetResourceName() string { 61 | args := m.Called() 62 | 63 | // Note that this is not checking if first argument is string, so "" (empty string) 64 | // can be used when we want to test failing GetResourceName. 65 | if args.Get(0) == nil { 66 | return m.Type 67 | } 68 | 69 | return args.String(0) 70 | } 71 | 72 | type conditionMock struct { 73 | mock.Mock 74 | } 75 | 76 | func (m *conditionMock) Type() string { 77 | args := m.Called() 78 | 79 | if args.Get(0) == nil { 80 | return basicConditionOne 81 | } 82 | return args.String(0) 83 | } 84 | 85 | func (m *conditionMock) Check(request *AccessRequest) error { 86 | args := m.Called() 87 | 88 | return args.Error(0) 89 | } 90 | 91 | func getBasicRoleOne() *Role { 92 | return &Role{ 93 | ID: basicRoleOneName, 94 | Description: "Basic Role", 95 | Grants: GrantsMap{ 96 | basicResourceOneName: { 97 | &Permission{Action: createAction}, 98 | &Permission{Action: readAction}, 99 | }, 100 | }, 101 | } 102 | } 103 | 104 | func getBasicRoleTwo() *Role { 105 | return &Role{ 106 | ID: basicRoleTwoName, 107 | Description: "Basic Role", 108 | Grants: GrantsMap{ 109 | basicResourceOneName: { 110 | &Permission{Action: createAction}, 111 | &Permission{Action: readAction}, 112 | &Permission{Action: updateAction}, 113 | &Permission{Action: deleteAction}, 114 | }, 115 | }, 116 | } 117 | } 118 | 119 | func getBasicParentRole() *Role { 120 | role := getBasicRoleOne() 121 | 122 | role.ID = basicParentRoleName 123 | role.Description = "Basic Parent Role" 124 | role.Grants[basicResourceOneName] = append(role.Grants[basicResourceOneName], &Permission{Action: updateAction}) 125 | 126 | return role 127 | } 128 | 129 | func getBasicPolicy() *PolicyDefinition { 130 | return &PolicyDefinition{ 131 | Roles: Roles{ 132 | basicRoleOneName: getBasicRoleOne(), 133 | }, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /condition_empty_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type emptyConditionSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestEmptyConditionSuite(t *testing.T) { 15 | suite.Run(t, new(emptyConditionSuite)) 16 | } 17 | 18 | func (s *emptyConditionSuite) TestType_Empty() { 19 | testCondition := &EmptyCondition{} 20 | 21 | assert.Equal(s.T(), EmptyConditionType, testCondition.Type()) 22 | } 23 | 24 | func (s *emptyConditionSuite) TestCheck_Empty() { 25 | testSubject := new(subjectMock) 26 | 27 | testSubject.FieldOne = "testValue" 28 | testSubject.FieldTwo = 1 29 | testSubject.FieldThree = nil 30 | 31 | testRequest := &AccessRequest{ 32 | Subject: testSubject, 33 | } 34 | 35 | // Failing descriptors - missing field 36 | testCondition := &EmptyCondition{ 37 | Value: &ValueDescriptor{Source: SubjectField}, 38 | } 39 | 40 | err := testCondition.Check(testRequest) 41 | 42 | assert.Error(s.T(), err) 43 | 44 | // Not empty 45 | testCondition.Value = &ValueDescriptor{ 46 | Source: SubjectField, 47 | Field: "FieldOne", 48 | } 49 | 50 | err = testCondition.Check(testRequest) 51 | 52 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), err) 53 | 54 | // Missing value field 55 | testCondition.Value = &ValueDescriptor{ 56 | Source: SubjectField, 57 | Field: "FieldThree", 58 | } 59 | 60 | err = testCondition.Check(testRequest) 61 | 62 | assert.Nil(s.T(), err) 63 | 64 | // 0 string value 65 | testSubject.FieldOne = "" 66 | testCondition.Value = &ValueDescriptor{ 67 | Source: SubjectField, 68 | Field: "FieldOne", 69 | } 70 | 71 | err = testCondition.Check(testRequest) 72 | 73 | assert.Nil(s.T(), err) 74 | 75 | // 0 int value 76 | testSubject.FieldTwo = 0 77 | testCondition.Value = &ValueDescriptor{ 78 | Source: SubjectField, 79 | Field: "FieldTwo", 80 | } 81 | 82 | err = testCondition.Check(testRequest) 83 | 84 | assert.Nil(s.T(), err) 85 | } 86 | 87 | func (s *emptyConditionSuite) TestType_NotEmpty() { 88 | testCondition := &NotEmptyCondition{} 89 | 90 | assert.Equal(s.T(), NotEmptyConditionType, testCondition.Type()) 91 | } 92 | 93 | func (s *emptyConditionSuite) TestCheck_NotEmpty() { 94 | testSubject := new(subjectMock) 95 | 96 | testSubject.FieldOne = "testValue" 97 | testSubject.FieldTwo = 1 98 | testSubject.FieldThree = nil 99 | 100 | testRequest := &AccessRequest{ 101 | Subject: testSubject, 102 | } 103 | 104 | // Failing descriptors - missing field 105 | testCondition := &NotEmptyCondition{ 106 | Value: &ValueDescriptor{Source: SubjectField}, 107 | } 108 | 109 | err := testCondition.Check(testRequest) 110 | 111 | assert.Error(s.T(), err) 112 | 113 | // Empty 114 | testSubject.FieldOne = "" 115 | testCondition.Value = &ValueDescriptor{ 116 | Source: SubjectField, 117 | Field: "FieldOne", 118 | } 119 | 120 | err = testCondition.Check(testRequest) 121 | 122 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), err) 123 | 124 | // Not empty 125 | testSubject.FieldOne = "testValue" 126 | testCondition.Value = &ValueDescriptor{ 127 | Source: SubjectField, 128 | Field: "FieldOne", 129 | } 130 | 131 | err = testCondition.Check(testRequest) 132 | 133 | assert.Nil(s.T(), err) 134 | } 135 | -------------------------------------------------------------------------------- /internal/tests/test_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionPresets": { 3 | "accessSelf": { 4 | "conditions": [ 5 | { 6 | "type": "EQUAL", 7 | "options": { 8 | "name": "self", 9 | "left": { 10 | "source": "ResourceField", 11 | "field": "ID" 12 | }, 13 | "right": { 14 | "source": "SubjectField", 15 | "field": "ID" 16 | } 17 | } 18 | } 19 | ] 20 | }, 21 | "readWhereBelongs": { 22 | "action": "read", 23 | "conditions": [ 24 | { 25 | "type": "BELONGS_TO", 26 | "options": {} 27 | } 28 | ] 29 | }, 30 | "updateOwn": { 31 | "action": "update", 32 | "conditions": [ 33 | { 34 | "type": "EQUAL", 35 | "options": { 36 | "name": "isOwner", 37 | "left": { 38 | "source": "ResourceField", 39 | "field": "CreatedBy" 40 | }, 41 | "right": { 42 | "source": "SubjectField", 43 | "field": "ID" 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | }, 50 | "roles": { 51 | "Admin": { 52 | "grants": { 53 | "Conversation": [ 54 | { 55 | "action": "read" 56 | } 57 | ], 58 | "User": [ 59 | { 60 | "action": "create" 61 | }, 62 | { 63 | "action": "read" 64 | }, 65 | { 66 | "action": "update" 67 | }, 68 | { 69 | "action": "delete" 70 | } 71 | ] 72 | }, 73 | "parents": [ 74 | "User" 75 | ] 76 | }, 77 | "BasicUser": { 78 | "grants": { 79 | "User": [ 80 | { 81 | "action": "read", 82 | "conditions": [ 83 | { 84 | "type": "EQUAL", 85 | "options": { 86 | "name": "self", 87 | "left": { 88 | "source": "ResourceField", 89 | "field": "ID" 90 | }, 91 | "right": { 92 | "source": "SubjectField", 93 | "field": "ID" 94 | } 95 | } 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | }, 102 | "User": { 103 | "grants": { 104 | "Conversation": [ 105 | { 106 | "action": "read", 107 | "conditions": [ 108 | { 109 | "type": "BELONGS_TO", 110 | "options": {} 111 | } 112 | ] 113 | }, 114 | { 115 | "action": "update", 116 | "conditions": [ 117 | { 118 | "type": "EQUAL", 119 | "options": { 120 | "name": "isOwner", 121 | "left": { 122 | "source": "ResourceField", 123 | "field": "CreatedBy" 124 | }, 125 | "right": { 126 | "source": "SubjectField", 127 | "field": "ID" 128 | } 129 | } 130 | } 131 | ] 132 | }, 133 | { 134 | "action": "create" 135 | }, 136 | { 137 | "action": "delete", 138 | "conditions": [ 139 | { 140 | "type": "EMPTY", 141 | "options": { 142 | "name": "deleteActive", 143 | "value": { 144 | "source": "ResourceField", 145 | "field": "Active" 146 | } 147 | } 148 | }, 149 | { 150 | "type": "GREATER_THAN", 151 | "options": { 152 | "value": { 153 | "source": "ResourceField", 154 | "field": "MessagesCount" 155 | } 156 | } 157 | } 158 | ] 159 | } 160 | ] 161 | }, 162 | "parents": [ 163 | "BasicUser" 164 | ] 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /value_source_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | type valueSourcesWrapper struct { 13 | Subject ValueSource `json:"subject" yaml:"subject"` 14 | Resource ValueSource `json:"resource" yaml:"resource"` 15 | Context ValueSource `json:"context" yaml:"context"` 16 | Explicit ValueSource `json:"explicit" yaml:"explicit"` 17 | } 18 | 19 | type valueSourceSuiteMock struct { 20 | suite.Suite 21 | } 22 | 23 | func TestValueSourceSuite(t *testing.T) { 24 | suite.Run(t, new(valueSourceSuiteMock)) 25 | } 26 | 27 | func (s *valueSourceSuiteMock) TestValueSourceString() { 28 | assert.Equal(s.T(), "SubjectField", SubjectField.String()) 29 | assert.Equal(s.T(), "ResourceField", ResourceField.String()) 30 | assert.Equal(s.T(), "ContextField", ContextField.String()) 31 | assert.Equal(s.T(), "Explicit", Explicit.String()) 32 | assert.Equal(s.T(), "", noopValueSource.String()) 33 | } 34 | 35 | func (s *valueSourceSuiteMock) TestUnmarshalJSON() { 36 | valueSourceData := []byte(`{ 37 | "subject": "SubjectField", 38 | "resource": "ResourceField", 39 | "context": "ContextField", 40 | "explicit": "Explicit" 41 | }`) 42 | 43 | assert.True(s.T(), json.Valid(valueSourceData)) 44 | 45 | testValueSources := &valueSourcesWrapper{} 46 | 47 | err := json.Unmarshal(valueSourceData, testValueSources) 48 | 49 | assert.Nil(s.T(), err) 50 | 51 | s.assertValueSourcesWrapper(testValueSources) 52 | } 53 | 54 | func (s *valueSourceSuiteMock) TestUnmarshalYAML() { 55 | valueSourceData := []byte(` 56 | subject: "SubjectField" 57 | resource: "ResourceField" 58 | context: "ContextField" 59 | explicit: "Explicit" 60 | `) 61 | 62 | testValueSources := &valueSourcesWrapper{} 63 | 64 | err := yaml.Unmarshal(valueSourceData, testValueSources) 65 | 66 | assert.Nil(s.T(), err) 67 | 68 | s.assertValueSourcesWrapper(testValueSources) 69 | } 70 | 71 | func (s *valueSourceSuiteMock) TestMarshalJSON() { 72 | testValueSources := &valueSourcesWrapper{ 73 | Subject: SubjectField, 74 | Resource: ResourceField, 75 | Context: ContextField, 76 | Explicit: Explicit, 77 | } 78 | 79 | valueSourcesJSON, err := json.Marshal(testValueSources) 80 | 81 | assert.Nil(s.T(), err) 82 | assert.True(s.T(), json.Valid(valueSourcesJSON)) 83 | 84 | testValueSources = &valueSourcesWrapper{} 85 | 86 | err = json.Unmarshal(valueSourcesJSON, testValueSources) 87 | 88 | assert.Nil(s.T(), err) 89 | 90 | s.assertValueSourcesWrapper(testValueSources) 91 | } 92 | 93 | func (s *valueSourceSuiteMock) TestMarshalYAML() { 94 | testValueSources := &valueSourcesWrapper{ 95 | Subject: SubjectField, 96 | Resource: ResourceField, 97 | Context: ContextField, 98 | Explicit: Explicit, 99 | } 100 | 101 | valueSourcesYAML, err := yaml.Marshal(testValueSources) 102 | 103 | assert.Nil(s.T(), err) 104 | 105 | testValueSources = &valueSourcesWrapper{} 106 | 107 | err = yaml.Unmarshal(valueSourcesYAML, testValueSources) 108 | 109 | assert.Nil(s.T(), err) 110 | 111 | s.assertValueSourcesWrapper(testValueSources) 112 | } 113 | 114 | func (s *valueSourceSuiteMock) assertValueSourcesWrapper(testValueSources *valueSourcesWrapper) { 115 | assert.Equal(s.T(), testValueSources.Subject, SubjectField) 116 | assert.Equal(s.T(), testValueSources.Resource, ResourceField) 117 | assert.Equal(s.T(), testValueSources.Context, ContextField) 118 | assert.Equal(s.T(), testValueSources.Explicit, Explicit) 119 | } 120 | -------------------------------------------------------------------------------- /value_descriptor_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type valueDescriptorSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestValueDescriptorSuite(t *testing.T) { 15 | suite.Run(t, new(valueDescriptorSuite)) 16 | } 17 | 18 | func (s *valueDescriptorSuite) TestGetValue_NilDescriptor() { 19 | var desc *ValueDescriptor = nil 20 | 21 | assert.NotPanics(s.T(), func() { desc.GetValue(&AccessRequest{}) }) //nolint 22 | 23 | value, err := desc.GetValue(&AccessRequest{}) 24 | 25 | assert.Nil(s.T(), value) 26 | 27 | assert.IsType(s.T(), new(ValueDescriptorMalformedError), err) 28 | 29 | } 30 | 31 | func (s *valueDescriptorSuite) TestGetValue_Explicit() { 32 | testRequest := &AccessRequest{} 33 | 34 | testDescriptor := &ValueDescriptor{ 35 | Source: Explicit, 36 | } 37 | 38 | value, err := testDescriptor.GetValue(testRequest) 39 | 40 | assert.Nil(s.T(), err) 41 | assert.Equal(s.T(), nil, value) 42 | 43 | testDescriptor.Value = 1 44 | 45 | value, err = testDescriptor.GetValue(testRequest) 46 | 47 | assert.Nil(s.T(), err) 48 | assert.Equal(s.T(), 1, value) 49 | } 50 | 51 | func (s *valueDescriptorSuite) TestGetValue_Subject() { 52 | testSubject := new(subjectMock) 53 | 54 | testSubject.FieldOne = "testValue" 55 | testSubject.FieldThree = []int{} 56 | 57 | testRequest := &AccessRequest{ 58 | Subject: testSubject, 59 | } 60 | 61 | testDescriptor := &ValueDescriptor{ 62 | Source: SubjectField, 63 | } 64 | 65 | value, err := testDescriptor.GetValue(testRequest) 66 | 67 | assert.IsType(s.T(), new(ValueDescriptorMalformedError), err) 68 | assert.Nil(s.T(), value) 69 | 70 | testDescriptor.Field = "IncorrectField" 71 | 72 | value, err = testDescriptor.GetValue(testRequest) 73 | 74 | assert.IsType(s.T(), new(ValueDescriptorMalformedError), err) 75 | assert.Nil(s.T(), value) 76 | 77 | testDescriptor.Field = "FieldOne" 78 | 79 | value, err = testDescriptor.GetValue(testRequest) 80 | 81 | assert.Nil(s.T(), err) 82 | assert.Equal(s.T(), testSubject.FieldOne, value) 83 | 84 | testDescriptor.Field = "FieldThree" 85 | 86 | value, err = testDescriptor.GetValue(testRequest) 87 | 88 | assert.Nil(s.T(), err) 89 | assert.Equal(s.T(), testSubject.FieldThree, value) 90 | } 91 | 92 | func (s *valueDescriptorSuite) TestGetValue_Resource() { 93 | testResource := new(resourceMock) 94 | 95 | testResource.FieldOne = "testValue" 96 | testResource.FieldThree = []int{} 97 | 98 | testRequest := &AccessRequest{ 99 | Resource: testResource, 100 | } 101 | 102 | testDescriptor := &ValueDescriptor{ 103 | Source: ResourceField, 104 | Field: "FieldOne", 105 | } 106 | 107 | value, err := testDescriptor.GetValue(testRequest) 108 | 109 | assert.Nil(s.T(), err) 110 | assert.Equal(s.T(), testResource.FieldOne, value) 111 | } 112 | 113 | func (s *valueDescriptorSuite) TestGetValue_Context() { 114 | testContext := Context{ 115 | "FieldOne": "testValue", 116 | "FieldTwo": 2, 117 | } 118 | 119 | testRequest := &AccessRequest{ 120 | Context: testContext, 121 | } 122 | 123 | testDescriptor := &ValueDescriptor{ 124 | Source: ContextField, 125 | Field: "FieldOne", 126 | } 127 | 128 | value, err := testDescriptor.GetValue(testRequest) 129 | 130 | assert.Nil(s.T(), err) 131 | assert.Equal(s.T(), testContext["FieldOne"], value) 132 | 133 | testDescriptor.Field = "FieldTwo" 134 | 135 | value, err = testDescriptor.GetValue(testRequest) 136 | 137 | assert.Nil(s.T(), err) 138 | assert.Equal(s.T(), testContext["FieldTwo"], value) 139 | } 140 | 141 | func (s *valueDescriptorSuite) TestGetValue_MissingSource() { 142 | testDescriptor := &ValueDescriptor{ 143 | Source: noopValueSource, 144 | Field: "TestField", 145 | } 146 | 147 | value, err := testDescriptor.GetValue(&AccessRequest{}) 148 | 149 | assert.Nil(s.T(), value) 150 | assert.IsType(s.T(), new(ValueDescriptorMalformedError), err) 151 | } 152 | -------------------------------------------------------------------------------- /condition_equal_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type equalConditionSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestEqualConditionSuite(t *testing.T) { 15 | suite.Run(t, new(equalConditionSuite)) 16 | } 17 | 18 | func (s *equalConditionSuite) TestType_Equal() { 19 | testCondition := &EqualCondition{} 20 | 21 | assert.Equal(s.T(), EqualConditionType, testCondition.Type()) 22 | } 23 | 24 | func (s *equalConditionSuite) TestType_NotEqual() { 25 | testCondition := &NotEqualCondition{} 26 | 27 | assert.Equal(s.T(), NotEqualConditionType, testCondition.Type()) 28 | } 29 | 30 | func (s *equalConditionSuite) TestCheck_Equal() { 31 | testSubject := new(subjectMock) 32 | 33 | testSubject.FieldOne = "testValue" 34 | testSubject.FieldTwo = 1 35 | 36 | testResource := new(resourceMock) 37 | 38 | testResource.FieldOne = "testValue" 39 | testResource.FieldTwo = 2 40 | 41 | testRequest := &AccessRequest{ 42 | Subject: testSubject, 43 | Resource: testResource, 44 | } 45 | 46 | // Failing descriptors - missing field 47 | testCondition := &EqualCondition{ 48 | Left: &ValueDescriptor{Source: SubjectField}, 49 | Right: &ValueDescriptor{Source: ResourceField}, 50 | } 51 | 52 | err := testCondition.Check(testRequest) 53 | 54 | assert.Error(s.T(), err) 55 | 56 | testCondition.Left = &ValueDescriptor{ 57 | Source: SubjectField, 58 | Field: "FieldOne", 59 | } 60 | 61 | err = testCondition.Check(testRequest) 62 | 63 | assert.Error(s.T(), err) 64 | 65 | testCondition.Right = &ValueDescriptor{ 66 | Source: ResourceField, 67 | Field: "FieldOne", 68 | } 69 | 70 | err = testCondition.Check(testRequest) 71 | 72 | assert.Nil(s.T(), err) 73 | 74 | // Error - different types 75 | testCondition.Right = &ValueDescriptor{ 76 | Source: ResourceField, 77 | Field: "FieldTwo", 78 | } 79 | 80 | err = testCondition.Check(testRequest) 81 | 82 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), err) 83 | 84 | // Error - different values 85 | testCondition.Left = &ValueDescriptor{ 86 | Source: SubjectField, 87 | Field: "FieldTwo", 88 | } 89 | 90 | err = testCondition.Check(testRequest) 91 | 92 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), err) 93 | } 94 | 95 | func (s *equalConditionSuite) TestCheck_NotEqual() { 96 | testSubject := new(subjectMock) 97 | 98 | testSubject.FieldOne = "testValue" 99 | testSubject.FieldTwo = 1 100 | 101 | testResource := new(resourceMock) 102 | 103 | testResource.FieldOne = "testValue" 104 | testResource.FieldTwo = 2 105 | 106 | testRequest := &AccessRequest{ 107 | Subject: testSubject, 108 | Resource: testResource, 109 | } 110 | 111 | // Failing descriptors - missing field 112 | testCondition := &NotEqualCondition{ 113 | Left: &ValueDescriptor{Source: SubjectField}, 114 | Right: &ValueDescriptor{Source: ResourceField}, 115 | } 116 | 117 | err := testCondition.Check(testRequest) 118 | 119 | assert.Error(s.T(), err) 120 | 121 | testCondition.Left = &ValueDescriptor{ 122 | Source: SubjectField, 123 | Field: "FieldOne", 124 | } 125 | 126 | err = testCondition.Check(testRequest) 127 | 128 | assert.Error(s.T(), err) 129 | 130 | testCondition.Right = &ValueDescriptor{ 131 | Source: ResourceField, 132 | Field: "FieldOne", 133 | } 134 | 135 | // Values are the same 136 | err = testCondition.Check(testRequest) 137 | 138 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), err) 139 | 140 | // Satisfied - different types 141 | testCondition.Right = &ValueDescriptor{ 142 | Source: ResourceField, 143 | Field: "FieldTwo", 144 | } 145 | 146 | err = testCondition.Check(testRequest) 147 | 148 | assert.Nil(s.T(), err) 149 | 150 | // Satisfied - different values 151 | testCondition.Left = &ValueDescriptor{ 152 | Source: SubjectField, 153 | Field: "FieldTwo", 154 | } 155 | 156 | err = testCondition.Check(testRequest) 157 | 158 | assert.Nil(s.T(), err) 159 | } 160 | -------------------------------------------------------------------------------- /adapters/file_adapter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/el-mike/restrict/v2" 5 | ) 6 | 7 | // AllowedFileType - alias type for describing allowed file types. 8 | type AllowedFileType string 9 | 10 | const ( 11 | // JSONFile - JSON file type token. 12 | JSONFile AllowedFileType = "JSONFile" 13 | // YAMLFile - YAML file type token. 14 | YAMLFile AllowedFileType = "YAMLFile" 15 | ) 16 | 17 | // defaultJSONIndent - default JSON file indentation. 18 | const defaultJSONIndent = "\t" 19 | 20 | // defaultFilePerm - default file's perm. 21 | const defaultFilePerm FilePerm = 0644 22 | 23 | // FileAdapter - StorageAdapter implementation, providing file-based persistence. 24 | // It can be configured to use JSON or YAML format. 25 | type FileAdapter struct { 26 | fileHandler FileReadWriter 27 | jsonHandler JSONMarshalUnmarshaler 28 | yamlHandler YAMLMarshalUnmarshaler 29 | 30 | fileName string 31 | fileType AllowedFileType 32 | filePerm FilePerm 33 | jsonIndent string 34 | } 35 | 36 | // NewFileAdapter - returns new FileAdapter instance. 37 | func NewFileAdapter(fileName string, fileType AllowedFileType) *FileAdapter { 38 | return &FileAdapter{ 39 | fileHandler: newDefaultFileHandler(), 40 | jsonHandler: newDefaultJSONHandler(), 41 | yamlHandler: newDefaultYAMLHandler(), 42 | 43 | fileName: fileName, 44 | fileType: fileType, 45 | filePerm: defaultFilePerm, 46 | jsonIndent: defaultJSONIndent, 47 | } 48 | } 49 | 50 | // SetJsonIndent - allows to set indentation used when marshaling the Policy into JSON. 51 | func (fa *FileAdapter) SetJSONIndent(indent string) { 52 | fa.jsonIndent = indent 53 | } 54 | 55 | // SetFilePerm - allows to set perm of the file the policy is written into. 56 | func (fa *FileAdapter) SetFilePerm(perm FilePerm) { 57 | fa.filePerm = perm 58 | } 59 | 60 | // LoadPolicy - loads and returns policy from file specified when creating FileAdapter. 61 | func (fa *FileAdapter) LoadPolicy() (*restrict.PolicyDefinition, error) { 62 | data, err := fa.fileHandler.ReadFile(fa.fileName) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | if fa.fileType == JSONFile { 68 | return fa.createFromJSON(data) 69 | } 70 | 71 | if fa.fileType == YAMLFile { 72 | return fa.createFromYAML(data) 73 | } 74 | 75 | return nil, newFileTypeNotSupportedError(string(fa.fileType)) 76 | } 77 | 78 | // createFromJSON - helper function for creating the policy from JSON data. 79 | func (fa *FileAdapter) createFromJSON(data []byte) (*restrict.PolicyDefinition, error) { 80 | var policy *restrict.PolicyDefinition 81 | 82 | if err := fa.jsonHandler.Unmarshal(data, &policy); err != nil { 83 | return nil, err 84 | } 85 | 86 | return policy, nil 87 | } 88 | 89 | // createFromYAML - helper function for creating the policy from YAML data. 90 | func (fa *FileAdapter) createFromYAML(data []byte) (*restrict.PolicyDefinition, error) { 91 | var policy *restrict.PolicyDefinition 92 | 93 | if err := fa.yamlHandler.Unmarshal(data, &policy); err != nil { 94 | return nil, err 95 | } 96 | 97 | return policy, nil 98 | } 99 | 100 | // SavePolicy - saves given policy in file specified when creating FileAdapter. 101 | func (fa *FileAdapter) SavePolicy(policy *restrict.PolicyDefinition) error { 102 | if fa.fileType == JSONFile { 103 | return fa.saveJSON(policy) 104 | } 105 | 106 | if fa.fileType == YAMLFile { 107 | return fa.saveYAML(policy) 108 | } 109 | 110 | return newFileTypeNotSupportedError(string(fa.fileType)) 111 | } 112 | 113 | // saveJSON - helper function for saving policy in JSON format. 114 | func (fa *FileAdapter) saveJSON(policy *restrict.PolicyDefinition) error { 115 | json, err := fa.jsonHandler.MarshalIndent(policy, "", fa.jsonIndent) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | if err := fa.saveFile(json); err != nil { 121 | return err 122 | } 123 | 124 | return nil 125 | } 126 | 127 | // saveYAML - helper function for saving policy in YAML format. 128 | func (fa *FileAdapter) saveYAML(policy *restrict.PolicyDefinition) error { 129 | yaml, err := fa.yamlHandler.Marshal(policy) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if err := fa.saveFile(yaml); err != nil { 135 | return err 136 | } 137 | 138 | return nil 139 | } 140 | 141 | // saveFile - saves content to file. 142 | func (fa *FileAdapter) saveFile(content []byte) error { 143 | return fa.fileHandler.WriteFile(fa.fileName, content, fa.filePerm) 144 | } 145 | -------------------------------------------------------------------------------- /condition.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // Condition - additional requirement that needs to be satisfied 10 | // to grant given permission. 11 | type Condition interface { 12 | // Type - returns Condition's type. 13 | Type() string 14 | 15 | // Check - returns true if Condition is satisfied by 16 | // given AccessRequest, false otherwise. 17 | Check(request *AccessRequest) error 18 | } 19 | 20 | // Conditions - alias type for Conditions array. 21 | type Conditions []Condition 22 | 23 | // AppendCondition - adds new Condition to Conditions slice. 24 | func (cs *Conditions) appendCondition(condition Condition) { 25 | if cs == nil { 26 | cs = &Conditions{} 27 | } 28 | 29 | *cs = append(*cs, condition) 30 | } 31 | 32 | // jsonMarshalableCondition - helper type for handling marshaling/unmarshaling 33 | // of JSON structures. 34 | type jsonMarshalableCondition struct { 35 | Type string `json:"type"` 36 | Options json.RawMessage `json:"options,omitempty"` 37 | } 38 | 39 | // yamlMarshalableCondition - helper type for handling marshaling/unmarshaling 40 | // of YAML structures. 41 | type yamlMarshalableCondition struct { 42 | Type string `yaml:"type"` 43 | Options yaml.Node `yaml:"options,omitempty"` 44 | } 45 | 46 | // MarshalJSON - marshals a map of Conditions to JSON data. 47 | func (cs Conditions) MarshalJSON() ([]byte, error) { 48 | result := []*jsonMarshalableCondition{} 49 | 50 | for _, condition := range cs { 51 | options, err := json.Marshal(condition) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | result = append(result, &jsonMarshalableCondition{ 57 | Type: condition.Type(), 58 | Options: json.RawMessage(options), 59 | }) 60 | } 61 | 62 | return json.Marshal(result) 63 | } 64 | 65 | // MarshalYAML - marshals a map of Conditions to YAML data. 66 | func (cs Conditions) MarshalYAML() (interface{}, error) { 67 | result := []*yamlMarshalableCondition{} 68 | 69 | for _, condition := range cs { 70 | options := yaml.Node{} 71 | 72 | if err := options.Encode(condition); err != nil { 73 | return nil, err 74 | } 75 | 76 | result = append(result, &yamlMarshalableCondition{ 77 | Type: condition.Type(), 78 | Options: options, 79 | }) 80 | } 81 | 82 | output := yaml.Node{} 83 | 84 | if err := output.Encode(result); err != nil { 85 | return nil, err 86 | } 87 | 88 | return output, nil 89 | } 90 | 91 | // UnmarshalJSON - unmarshals a JSON-coded map of Conditions. 92 | func (cs *Conditions) UnmarshalJSON(jsonData []byte) error { 93 | var jsonValue []jsonMarshalableCondition 94 | 95 | if err := json.Unmarshal(jsonData, &jsonValue); err != nil { 96 | return err 97 | } 98 | 99 | for _, jsonCondition := range jsonValue { 100 | factory := ConditionFactories[jsonCondition.Type] 101 | 102 | if factory == nil { 103 | return newConditionFactoryNotFoundError(jsonCondition.Type) 104 | } 105 | 106 | condition := factory() 107 | 108 | if len(jsonCondition.Options) > 0 { 109 | if err := json.Unmarshal(jsonCondition.Options, condition); err != nil { 110 | return err 111 | } 112 | } 113 | 114 | cs.appendCondition(condition) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // UnmarshalYAML - unmarshals a YAML-coded map of Conditions. 121 | func (cs *Conditions) UnmarshalYAML(value *yaml.Node) error { 122 | var yamlValue []yamlMarshalableCondition 123 | 124 | if err := value.Decode(&yamlValue); err != nil { 125 | return err 126 | } 127 | 128 | for _, yamlCondition := range yamlValue { 129 | // Guard for conditions maps being empty - YAML will still 130 | // create a Nodes from them. 131 | if yamlCondition.Type == "" { 132 | continue 133 | } 134 | 135 | factory := ConditionFactories[yamlCondition.Type] 136 | 137 | if factory == nil { 138 | return newConditionFactoryNotFoundError(yamlCondition.Type) 139 | } 140 | 141 | condition := factory() 142 | 143 | if len(yamlCondition.Options.Content) > 0 { 144 | if err := yamlCondition.Options.Decode(condition); err != nil { 145 | return err 146 | } 147 | } 148 | 149 | cs.appendCondition(condition) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | // ConditionFactory - factory function for Condition. 156 | type ConditionFactory func() Condition 157 | 158 | // ConditionFactoriesMap - map of Condition factories. 159 | type ConditionFactoriesMap = map[string]ConditionFactory 160 | 161 | // ConditionFactories - stores a map of functions responsible for 162 | // creating new Conditions, based on their names. 163 | var ConditionFactories = ConditionFactoriesMap{ 164 | EqualConditionType: func() Condition { 165 | return new(EqualCondition) 166 | }, 167 | NotEqualConditionType: func() Condition { 168 | return new(NotEqualCondition) 169 | }, 170 | EmptyConditionType: func() Condition { 171 | return new(EmptyCondition) 172 | }, 173 | NotEmptyConditionType: func() Condition { 174 | return new(NotEmptyCondition) 175 | }, 176 | } 177 | 178 | // RegisterConditionFactory - adds a new ConditionFactory under given name. If given name 179 | // is already taken, an error is returned. 180 | func RegisterConditionFactory(name string, factory ConditionFactory) error { 181 | if ConditionFactories[name] != nil { 182 | return newConditionFactoryAlreadyExistsError(name) 183 | } 184 | 185 | ConditionFactories[name] = factory 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /authorization_errors.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "fmt" 5 | "github.com/el-mike/restrict/v2/internal/utils" 6 | "strings" 7 | ) 8 | 9 | // PermissionErrors - an alias type for a slice of PermissionError, with extra helper methods. 10 | type PermissionErrors []*PermissionError 11 | 12 | // GetByRoleName - returns PermissionError structs specific to given Role. 13 | func (ae PermissionErrors) GetByRoleName(roleName string) PermissionErrors { 14 | if ae == nil { 15 | return nil 16 | } 17 | 18 | result := PermissionErrors{} 19 | 20 | for _, e := range ae { 21 | if e.RoleName == roleName { 22 | result = append(result, e) 23 | } 24 | } 25 | 26 | return result 27 | } 28 | 29 | // GetByAction - returns PermissionError structs specific to given Action. 30 | func (ae PermissionErrors) GetByAction(action string) PermissionErrors { 31 | if ae == nil { 32 | return nil 33 | } 34 | 35 | result := PermissionErrors{} 36 | 37 | for _, e := range ae { 38 | if e.Action == action { 39 | result = append(result, e) 40 | } 41 | } 42 | 43 | return result 44 | } 45 | 46 | // GetFailedActions - returns all Actions for which access was denied. 47 | func (ae PermissionErrors) GetFailedActions() []string { 48 | actions := []string{} 49 | 50 | for _, e := range ae { 51 | if !utils.StringSliceContains(actions, e.Action) { 52 | actions = append(actions, e.Action) 53 | } 54 | } 55 | 56 | return actions 57 | } 58 | 59 | // AccessDeniedError - thrown when AccessRequest could not be satisfied due to 60 | // insufficient privileges. 61 | type AccessDeniedError struct { 62 | Request *AccessRequest 63 | Reasons PermissionErrors 64 | } 65 | 66 | // newAccessDeniedError - returns new AccessDeniedError instance. 67 | func newAccessDeniedError(request *AccessRequest, reasons PermissionErrors) *AccessDeniedError { 68 | return &AccessDeniedError{ 69 | Request: request, 70 | Reasons: reasons, 71 | } 72 | } 73 | 74 | // Error - error interface implementation. 75 | func (e *AccessDeniedError) Error() string { 76 | preparedActions := []string{} 77 | 78 | for _, action := range e.Reasons.GetFailedActions() { 79 | preparedActions = append(preparedActions, fmt.Sprintf("\"%s\"", action)) 80 | } 81 | 82 | actionsNoun := "Action" 83 | if len(preparedActions) > 1 { 84 | actionsNoun = "Actions" 85 | } 86 | 87 | return fmt.Sprintf( 88 | "access denied for %s: %s on Resource: \"%s\"", 89 | actionsNoun, 90 | strings.Join(preparedActions, ", "), 91 | e.Request.Resource.GetResourceName(), 92 | ) 93 | } 94 | 95 | // FirstReason - returns the first PermissionError encountered when performing authorization. 96 | // Especially helpful when AccessRequest was set to fail early. 97 | func (e *AccessDeniedError) FirstReason() *PermissionError { 98 | if len(e.Reasons) == 0 { 99 | return nil 100 | } 101 | 102 | return e.Reasons[0] 103 | } 104 | 105 | // ConditionErrors - an alias type for a slice of ConditionNotSatisfiedError. 106 | type ConditionErrors []*ConditionNotSatisfiedError 107 | 108 | // PermissionError - thrown when Permission is not granted for a given Action. 109 | type PermissionError struct { 110 | Action string 111 | RoleName string 112 | ResourceName string 113 | ConditionErrors ConditionErrors 114 | } 115 | 116 | // newPermissionError - returns new PermissionError instance. 117 | func newPermissionError(action, roleName, resourceName string, conditionErrors ConditionErrors) *PermissionError { 118 | return &PermissionError{ 119 | Action: action, 120 | RoleName: roleName, 121 | ResourceName: resourceName, 122 | ConditionErrors: conditionErrors, 123 | } 124 | } 125 | 126 | // Error - error interface implementation. 127 | func (e *PermissionError) Error() string { 128 | if len(e.ConditionErrors) == 0 { 129 | return fmt.Sprintf("Permission for Action: \"%v\" is not granted for Resource: \"%v\"", e.Action, e.ResourceName) 130 | } 131 | 132 | return fmt.Sprintf( 133 | "Permission for Action: \"%v\" was denied for Resource: \"%v\" due to failed Conditions", 134 | e.Action, 135 | e.ResourceName, 136 | ) 137 | } 138 | 139 | // FirstConditionError - returns the first ConditionNotSatisfiedError encountered when validating given Action. 140 | // Especially helpful when AccessRequest was set to fail early. 141 | func (e *PermissionError) FirstConditionError() *ConditionNotSatisfiedError { 142 | if len(e.ConditionErrors) == 0 { 143 | return nil 144 | } 145 | 146 | return e.ConditionErrors[0] 147 | } 148 | 149 | // HasFailedConditions - returns true if error was due to failed Conditions, false otherwise. 150 | func (e *PermissionError) HasFailedConditions() bool { 151 | return len(e.ConditionErrors) > 0 152 | } 153 | 154 | // ConditionNotSatisfiedError - thrown when given Condition for given AccessRequest. 155 | type ConditionNotSatisfiedError struct { 156 | Condition Condition 157 | Request *AccessRequest 158 | Reason error 159 | } 160 | 161 | // NewConditionNotSatisfiedError - returns new ConditionNotSatisfiedError instance. 162 | func NewConditionNotSatisfiedError(condition Condition, request *AccessRequest, reason error) *ConditionNotSatisfiedError { 163 | return &ConditionNotSatisfiedError{ 164 | Condition: condition, 165 | Request: request, 166 | Reason: reason, 167 | } 168 | } 169 | 170 | // Error - error interface implementation. 171 | func (e *ConditionNotSatisfiedError) Error() string { 172 | return fmt.Sprintf("Condition: \"%v\" was not satisfied, reason: %s", e.Condition.Type(), e.Reason.Error()) 173 | } 174 | -------------------------------------------------------------------------------- /common_errors.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import "fmt" 4 | 5 | // RoleNotFoundError - thrown when there is an operation called for a Role 6 | // that does not exist. 7 | type RoleNotFoundError struct { 8 | roleID string 9 | } 10 | 11 | // newRoleNotFoundError - returns new RoleNotFoundError instance. 12 | func newRoleNotFoundError(roleID string) *RoleNotFoundError { 13 | return &RoleNotFoundError{ 14 | roleID: roleID, 15 | } 16 | } 17 | 18 | // Error - error interface implementation. 19 | func (e *RoleNotFoundError) Error() string { 20 | return fmt.Sprintf("Role with ID: \"%s\" has not been found", e.roleID) 21 | } 22 | 23 | // RoleAlreadyExistsError - thrown when new Role is being added with 24 | // ID that already exists in the PolicyDefinition. 25 | type RoleAlreadyExistsError struct { 26 | roleID string 27 | } 28 | 29 | // newRoleAlreadyExistsError - returns new RoleAlreadyExistsError instance. 30 | func newRoleAlreadyExistsError(roleID string) *RoleAlreadyExistsError { 31 | return &RoleAlreadyExistsError{ 32 | roleID: roleID, 33 | } 34 | } 35 | 36 | // Error - error interface implementation. 37 | func (e *RoleAlreadyExistsError) Error() string { 38 | return fmt.Sprintf("Role with ID: \"%s\" already exists", e.roleID) 39 | } 40 | 41 | // PermissionPresetNotFoundError - thrown when Permission specifies a preset which is not 42 | // defined in PermissionPresets on PolicyDefinition. 43 | type PermissionPresetNotFoundError struct { 44 | name string 45 | } 46 | 47 | // newPermissionPresetNotFoundError - returns new PermissionPresetNotFoundError instance. 48 | func newPermissionPresetNotFoundError(name string) *PermissionPresetNotFoundError { 49 | return &PermissionPresetNotFoundError{ 50 | name: name, 51 | } 52 | } 53 | 54 | // Error - error interface implementation. 55 | func (e *PermissionPresetNotFoundError) Error() string { 56 | return fmt.Sprintf("Permission preset: \"%s\" has not been found", e.name) 57 | } 58 | 59 | // PermissionPresetAlreadyExistsError - thrown when a new Permission preset is being added 60 | // with a name (key) that already exists. 61 | type PermissionPresetAlreadyExistsError struct { 62 | name string 63 | } 64 | 65 | // newPermissionPresetAlreadyExistsError - returns new PermissionPresetAlreadyExistsError instance. 66 | func newPermissionPresetAlreadyExistsError(name string) *PermissionPresetAlreadyExistsError { 67 | return &PermissionPresetAlreadyExistsError{ 68 | name: name, 69 | } 70 | } 71 | 72 | func (e *PermissionPresetAlreadyExistsError) Error() string { 73 | return fmt.Sprintf("Permission preset with name: \"%s\" already exists", e.name) 74 | } 75 | 76 | // RequestMalformedError - thrown when AccessRequest is not correct or 77 | // does not contain all necessary information. 78 | type RequestMalformedError struct { 79 | request *AccessRequest 80 | reason error 81 | } 82 | 83 | // newRequestMalformedError - returns new SubjectNotDefinedError instance. 84 | func newRequestMalformedError(request *AccessRequest, reason error) *RequestMalformedError { 85 | return &RequestMalformedError{ 86 | request: request, 87 | reason: reason, 88 | } 89 | } 90 | 91 | // Error - error interface implementation. 92 | func (e *RequestMalformedError) Error() string { 93 | return "Subject is not defined" 94 | } 95 | 96 | // Reason - returns underlying reason (an error) of malformed Request. 97 | func (e *RequestMalformedError) Reason() error { 98 | return e.reason 99 | } 100 | 101 | // FailedRequest - returns an AccessRequest for which access has been denied. 102 | func (e *RequestMalformedError) FailedRequest() *AccessRequest { 103 | return e.request 104 | } 105 | 106 | // ConditionFactoryAlreadyExistsError - thrown when ConditionFactory is being added under a name 107 | // that's already set in ConditionFactories map. 108 | type ConditionFactoryAlreadyExistsError struct { 109 | conditionName string 110 | } 111 | 112 | // newConditionFactoryAlreadyExistsError - returns new ConditionFactoryAlreadyExistsError instance. 113 | func newConditionFactoryAlreadyExistsError(conditionName string) *ConditionFactoryAlreadyExistsError { 114 | return &ConditionFactoryAlreadyExistsError{ 115 | conditionName: conditionName, 116 | } 117 | } 118 | 119 | // Error - error interface implementation. 120 | func (e *ConditionFactoryAlreadyExistsError) Error() string { 121 | return fmt.Sprintf("ConditionFactory for Condition: \"%v\" already exists", e.conditionName) 122 | } 123 | 124 | // ConditionFactoryNotFoundError - thrown when ConditionFactory is not found while 125 | // unmarshaling a Permission. 126 | type ConditionFactoryNotFoundError struct { 127 | conditionName string 128 | } 129 | 130 | // newConditionFactoryNotFoundError - returns new ConditionFactoryNotFoundError instance. 131 | func newConditionFactoryNotFoundError(conditionName string) *ConditionFactoryNotFoundError { 132 | return &ConditionFactoryNotFoundError{ 133 | conditionName: conditionName, 134 | } 135 | } 136 | 137 | // Error - error interface implementation. 138 | func (e *ConditionFactoryNotFoundError) Error() string { 139 | return fmt.Sprintf("ConditionFactory not found for Condition: \"%v\"", e.conditionName) 140 | } 141 | 142 | // ValueDescriptorMalformedError - thrown when malformed ValueDescriptor is being resolved. 143 | type ValueDescriptorMalformedError struct { 144 | descriptor *ValueDescriptor 145 | reason error 146 | } 147 | 148 | // newValueDescriptorMalformedError - returns new ValueDescriptorMalformedError instance. 149 | func newValueDescriptorMalformedError(descriptor *ValueDescriptor, reason error) *ValueDescriptorMalformedError { 150 | return &ValueDescriptorMalformedError{ 151 | descriptor: descriptor, 152 | reason: reason, 153 | } 154 | } 155 | 156 | // Error - error interface implementation. 157 | func (e *ValueDescriptorMalformedError) Error() string { 158 | return fmt.Sprintf("ValueDescriptor could not be resolved. Reason: %s", e.reason.Error()) 159 | } 160 | 161 | // Reason - returns underlying reason (an error) of malformed ValueDescriptor. 162 | func (e *ValueDescriptorMalformedError) Reason() error { 163 | return e.reason 164 | } 165 | 166 | // FailedDescriptor - returns failed ValueDescriptor. 167 | func (e *ValueDescriptorMalformedError) FailedDescriptor() *ValueDescriptor { 168 | return e.descriptor 169 | } 170 | 171 | // RoleInheritanceCycleError - thrown when circular Role inheritance is detected. 172 | type RoleInheritanceCycleError struct { 173 | roles []string 174 | } 175 | 176 | // newRoleInheritanceCycleError - returns new RoleInheritanceCycleError instance. 177 | func newRoleInheritanceCycleError(roles []string) *RoleInheritanceCycleError { 178 | return &RoleInheritanceCycleError{ 179 | roles: roles, 180 | } 181 | } 182 | 183 | // Error - error interface implementation. 184 | func (e *RoleInheritanceCycleError) Error() string { 185 | message := "Role inheritance cycle has been detected: " 186 | 187 | for i, role := range e.roles { 188 | if i > 0 { 189 | message += " -> " 190 | } 191 | 192 | message += fmt.Sprintf("\"%s\"", role) 193 | } 194 | 195 | // We want to add the first role at the end, to indicate the cycle. 196 | message += fmt.Sprintf(" -> \"%s\"", e.roles[0]) 197 | 198 | return message 199 | } 200 | -------------------------------------------------------------------------------- /access_manager.go: -------------------------------------------------------------------------------- 1 | // Package restrict provides an authorization library, with a hybrid of RBAC and ABAC models. 2 | package restrict 3 | 4 | import ( 5 | "fmt" 6 | "github.com/el-mike/restrict/v2/internal/utils" 7 | ) 8 | 9 | // AccessManager - an entity responsible for checking the authorization. It uses underlying 10 | // PolicyProvider to test an AccessRequest against currently used PolicyDefinition. 11 | type AccessManager struct { 12 | // PolicyProvider instance, responsible for providing PolicyDefinition. 13 | policyManager PolicyProvider 14 | } 15 | 16 | // NewAccessManager - returns new AccessManager instance. 17 | func NewAccessManager(policyManager PolicyProvider) *AccessManager { 18 | return &AccessManager{ 19 | policyManager: policyManager, 20 | } 21 | } 22 | 23 | // Authorize - checks if given AccessRequest can be satisfied given currently loaded policy. 24 | // Returns an error if access is not granted or any other problem occurred, nil otherwise. 25 | func (am *AccessManager) Authorize(request *AccessRequest) error { 26 | if request.Subject == nil || request.Resource == nil { 27 | return newRequestMalformedError(request, fmt.Errorf("Subject or Resource not defined")) 28 | } 29 | 30 | roles := request.Subject.GetRoles() 31 | resourceName := request.Resource.GetResourceName() 32 | 33 | if len(roles) == 0 || resourceName == "" { 34 | return newRequestMalformedError(request, fmt.Errorf("missing roles or resourceName")) 35 | } 36 | 37 | allPermissionErrors := PermissionErrors{} 38 | 39 | for _, roleName := range roles { 40 | permissionErrors, err := am.authorize(request, roleName, resourceName, []string{}) 41 | 42 | // If error is not authorization-specific, we return immediately. 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // If AccessRequest is satisfied by a Role, we return immediately. 48 | if permissionErrors == nil { 49 | return nil 50 | } 51 | 52 | // Otherwise, we save it to PermissionErrors, so we can return it to the caller 53 | // if no Role satisfies the AccessRequest. 54 | allPermissionErrors = append(allPermissionErrors, permissionErrors...) 55 | } 56 | 57 | return newAccessDeniedError(request, allPermissionErrors) 58 | } 59 | 60 | // authorize - helper function for decoupling role and resource names retrieval from recursive search. 61 | func (am *AccessManager) authorize(request *AccessRequest, roleName, resourceName string, checkedRoles []string) (PermissionErrors, error) { 62 | role, err := am.policyManager.GetRole(roleName) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | var grants Permissions 68 | 69 | if role.Grants == nil { 70 | grants = Permissions{} 71 | } else { 72 | grants = role.Grants[resourceName] 73 | } 74 | 75 | parents := role.Parents 76 | allPermissionErrors := PermissionErrors{} 77 | 78 | for _, action := range request.Actions { 79 | if action == "" { 80 | return nil, newRequestMalformedError(request, fmt.Errorf("action cannot be empty")) 81 | } 82 | 83 | permissionErrors, err := am.validateAction(grants, action, roleName, request) 84 | // If non-policy related error happened, we return it directly. 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // If access is not granted for given action on current Role, check if 90 | // any parent Role can satisfy the request. 91 | if permissionErrors != nil && len(parents) > 0 { 92 | checkedRoles = append(checkedRoles, roleName) 93 | 94 | for _, parent := range parents { 95 | parentRequest := &AccessRequest{ 96 | Subject: request.Subject, 97 | Resource: request.Resource, 98 | Actions: []string{action}, 99 | Context: request.Context, 100 | } 101 | 102 | // If parent has already been checked, we want to return an error - otherwise 103 | // this function will fall into infinite loop. 104 | if utils.StringSliceContains(checkedRoles, parent) { 105 | return nil, newRoleInheritanceCycleError(checkedRoles) 106 | } 107 | 108 | parentPermissionErrors, err := am.authorize(parentRequest, parent, resourceName, checkedRoles) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | // If .authorize call with parent Role has returned nil, 114 | // that means the request is satisfied. 115 | // Otherwise, returned errors should not override the original ones. 116 | if parentPermissionErrors == nil { 117 | permissionErrors = nil 118 | } 119 | } 120 | } 121 | 122 | // If request has not been granted, abort the loop and return an error, 123 | // skipping rest of the Actions in the request. 124 | if permissionErrors != nil { 125 | allPermissionErrors = append(allPermissionErrors, permissionErrors...) 126 | 127 | // If CompleteValidation is false, we want to return early. 128 | // Otherwise, loop will continue for the rest of the actions. 129 | if !request.CompleteValidation { 130 | return allPermissionErrors, nil 131 | } 132 | } 133 | } 134 | 135 | if len(allPermissionErrors) > 0 { 136 | return allPermissionErrors, nil 137 | } 138 | 139 | return nil, nil 140 | } 141 | 142 | // validateAction - checks whether a Permission is granted for Action. 143 | func (am *AccessManager) validateAction(permissions []*Permission, action, roleName string, request *AccessRequest) (PermissionErrors, error) { 144 | permissionErrors := PermissionErrors{} 145 | 146 | for _, permission := range permissions { 147 | if permission.Action == action { 148 | // If a Permission with given Action is found, and has no Conditions, access should be granted. 149 | if len(permission.Conditions) == 0 || request.SkipConditions { 150 | return nil, nil 151 | } 152 | 153 | conditionErrors, err := am.checkConditions(permission, request) 154 | // If non-policy related error happened, we return it directly. 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | // If error is nil, Conditions have been satisfied. 160 | if conditionErrors == nil { 161 | return nil, nil 162 | } 163 | 164 | // Otherwise, we add new PermissionError to result slice. 165 | permissionError := newPermissionError(action, roleName, request.Resource.GetResourceName(), conditionErrors) 166 | permissionErrors = append(permissionErrors, permissionError) 167 | } 168 | } 169 | 170 | // If there are no permissionErrors at this point, this means there was no Permission 171 | // for given Action (even one with failing Conditions). 172 | // In such case, a PermissionError with no ConditionErrors is added to reflect that. 173 | if len(permissionErrors) == 0 { 174 | permissionError := newPermissionError(action, roleName, request.Resource.GetResourceName(), nil) 175 | permissionErrors = append(permissionErrors, permissionError) 176 | } 177 | 178 | return permissionErrors, nil 179 | } 180 | 181 | // checkConditions - returns nil if all conditions specified for given actions 182 | // are satisfied, error otherwise. 183 | func (am *AccessManager) checkConditions(permission *Permission, request *AccessRequest) (ConditionErrors, error) { 184 | if permission.Conditions == nil { 185 | return nil, nil 186 | } 187 | 188 | conditionErrors := ConditionErrors{} 189 | 190 | for _, condition := range permission.Conditions { 191 | if err := condition.Check(request); err != nil { 192 | // If error returned is ConditionNotSatisfiedError, we add it to the result slice. 193 | // Otherwise, we want to abort immediately and return it directly. 194 | if conditionError, ok := err.(*ConditionNotSatisfiedError); ok { 195 | conditionErrors = append(conditionErrors, conditionError) 196 | 197 | // If CompleteValidation is not enabled, we return first encountered error. 198 | if !request.CompleteValidation { 199 | break 200 | } 201 | } else { 202 | return nil, err 203 | } 204 | } 205 | } 206 | 207 | if len(conditionErrors) > 0 { 208 | return conditionErrors, nil 209 | } 210 | 211 | return nil, nil 212 | } 213 | -------------------------------------------------------------------------------- /internal/tests/integration_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/el-mike/restrict/v2" 9 | "github.com/el-mike/restrict/v2/adapters" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type integrationSuite struct { 15 | suite.Suite 16 | 17 | testUserId string 18 | } 19 | 20 | func TestPoliciesSuite(t *testing.T) { 21 | suite.Run(t, new(integrationSuite)) 22 | } 23 | 24 | func (s *integrationSuite) SetupSuite() { 25 | s.testUserId = "testUser1" 26 | 27 | //nolint 28 | restrict.RegisterConditionFactory(hasUserConditionType, func() restrict.Condition { 29 | return new(hasUserCondition) 30 | }) 31 | 32 | //nolint 33 | restrict.RegisterConditionFactory(greatherThanType, func() restrict.Condition { 34 | return new(greaterThanCondition) 35 | }) 36 | } 37 | 38 | func (s *integrationSuite) TestRestrict_JSON() { 39 | jsonAdapter := adapters.NewFileAdapter("test_policy.json", adapters.JSONFile) 40 | 41 | policyManager, err := restrict.NewPolicyManager(jsonAdapter, true) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | s.testPolicy(policyManager) 47 | } 48 | 49 | func (s *integrationSuite) TestRestrict_YAML() { 50 | yamlAdapter := adapters.NewFileAdapter("test_policy.yaml", adapters.YAMLFile) 51 | 52 | policyManager, err := restrict.NewPolicyManager(yamlAdapter, true) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | s.testPolicy(policyManager) 58 | } 59 | 60 | func (s *integrationSuite) TestRestrict() { 61 | policyManager, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(PolicyOne), true) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | s.testPolicy(policyManager) 67 | } 68 | 69 | func (s *integrationSuite) testPolicy(policyManager *restrict.PolicyManager) { 70 | user := &User{ 71 | ID: s.testUserId, 72 | Roles: []string{UserRole}, 73 | } 74 | 75 | conversation := &Conversation{ 76 | ID: "testConversation1", 77 | CreatedBy: "otherUser1", 78 | Participants: []string{}, 79 | Active: true, 80 | } 81 | 82 | manager := restrict.NewAccessManager(policyManager) 83 | 84 | // "read" is not granted - User does not belong to the Conversation. 85 | err := manager.Authorize(&restrict.AccessRequest{ 86 | Subject: user, 87 | Resource: conversation, 88 | Actions: []string{"read"}, 89 | }) 90 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 91 | 92 | permissionErr := err.(*restrict.AccessDeniedError).FirstReason() 93 | conditionErr := permissionErr.FirstConditionError() 94 | 95 | assert.IsType(s.T(), new(restrict.PermissionError), permissionErr) 96 | assert.IsType(s.T(), new(restrict.ConditionNotSatisfiedError), conditionErr) 97 | 98 | // "read" granted - User belongs to the Conversation. 99 | conversation.Participants = []string{s.testUserId} 100 | 101 | err = manager.Authorize(&restrict.AccessRequest{ 102 | Subject: user, 103 | Resource: conversation, 104 | Actions: []string{"read"}, 105 | }) 106 | 107 | assert.Nil(s.T(), err) 108 | 109 | // "update" granted - User owns the conversation. 110 | conversation.CreatedBy = s.testUserId 111 | 112 | err = manager.Authorize(&restrict.AccessRequest{ 113 | Subject: user, 114 | Resource: conversation, 115 | Actions: []string{"update"}, 116 | }) 117 | 118 | fmt.Print(err) 119 | 120 | assert.Nil(s.T(), err) 121 | 122 | // "modify" is not granted. 123 | err = manager.Authorize(&restrict.AccessRequest{ 124 | Subject: user, 125 | Resource: conversation, 126 | Actions: []string{"read", "modify"}, 127 | }) 128 | 129 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 130 | assert.IsType(s.T(), new(restrict.PermissionError), err.(*restrict.AccessDeniedError).FirstReason()) 131 | 132 | // "delete" condition not satisfied - Conversation must be inactive. 133 | err = manager.Authorize(&restrict.AccessRequest{ 134 | Subject: user, 135 | Resource: conversation, 136 | Actions: []string{"delete"}, 137 | }) 138 | 139 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 140 | 141 | permissionErr = err.(*restrict.AccessDeniedError).FirstReason() 142 | conditionErr = permissionErr.FirstConditionError() 143 | 144 | assert.IsType(s.T(), new(restrict.ConditionNotSatisfiedError), conditionErr) 145 | 146 | condition := conditionErr.Condition.(*restrict.EmptyCondition) 147 | assert.Equal(s.T(), "deleteActive", condition.ID) 148 | 149 | // "delete" condition not satisfied - Conversation has to have less than 100 messages. 150 | conversation.Active = false 151 | conversation.MessagesCount = 110 152 | 153 | err = manager.Authorize(&restrict.AccessRequest{ 154 | Subject: user, 155 | Resource: conversation, 156 | Actions: []string{"delete"}, 157 | Context: restrict.Context{ 158 | "Max": 100, 159 | }, 160 | }) 161 | 162 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 163 | 164 | // User CAN read itself 165 | err = manager.Authorize(&restrict.AccessRequest{ 166 | Subject: user, 167 | Resource: user, 168 | Actions: []string{"read"}, 169 | }) 170 | 171 | assert.Nil(s.T(), err) 172 | 173 | // User can NOT create other users 174 | err = manager.Authorize(&restrict.AccessRequest{ 175 | Subject: user, 176 | Resource: &User{}, 177 | Actions: []string{"create"}, 178 | }) 179 | 180 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 181 | 182 | // Admin CAN create other users 183 | admin := &User{ 184 | ID: "admin1", 185 | Roles: []string{AdminRole}, 186 | } 187 | 188 | err = manager.Authorize(&restrict.AccessRequest{ 189 | Subject: admin, 190 | Resource: &User{}, 191 | Actions: []string{"create"}, 192 | }) 193 | 194 | assert.Nil(s.T(), err) 195 | 196 | // Admin CAN create Conversation because inherits from User. 197 | err = manager.Authorize(&restrict.AccessRequest{ 198 | Subject: admin, 199 | Resource: &Conversation{}, 200 | Actions: []string{"create"}, 201 | }) 202 | 203 | assert.Nil(s.T(), err) 204 | 205 | // Admin CAN read any Conversation, because it has unconditional read permission 206 | // (along with conditional one inherited from User). 207 | err = manager.Authorize(&restrict.AccessRequest{ 208 | Subject: admin, 209 | Resource: conversation, 210 | Actions: []string{"read"}, 211 | }) 212 | 213 | assert.Nil(s.T(), err) 214 | } 215 | 216 | func (s *integrationSuite) TestRestrict_CompleteValidation() { 217 | policyManager, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(PolicyOne), true) 218 | if err != nil { 219 | log.Fatal(err) 220 | } 221 | 222 | manager := restrict.NewAccessManager(policyManager) 223 | 224 | user := &User{ 225 | ID: s.testUserId, 226 | Roles: []string{UserRole}, 227 | } 228 | 229 | conversation := &Conversation{ 230 | ID: "testConversation1", 231 | CreatedBy: "otherUser1", 232 | Participants: []string{}, 233 | Active: true, 234 | MessagesCount: 20, 235 | } 236 | 237 | err = manager.Authorize(&restrict.AccessRequest{ 238 | Subject: user, 239 | Resource: conversation, 240 | Actions: []string{"read", "update", "delete"}, 241 | CompleteValidation: true, 242 | Context: restrict.Context{ 243 | "Max": 10, 244 | }, 245 | }) 246 | 247 | assert.IsType(s.T(), new(restrict.AccessDeniedError), err) 248 | 249 | accessErr := err.(*restrict.AccessDeniedError) 250 | 251 | // Expect 3 permission errors, one per each Action. 252 | assert.Equal(s.T(), 3, len(accessErr.Reasons)) 253 | 254 | permissionErrors := accessErr.Reasons 255 | 256 | // "read" action failing with unsatisfied hasUserCondition (readWhereBelongs preset). 257 | assert.Equal(s.T(), "read", permissionErrors[0].Action) 258 | assert.Equal(s.T(), 1, len(permissionErrors[0].ConditionErrors)) 259 | assert.IsType(s.T(), new(hasUserCondition), permissionErrors[0].ConditionErrors[0].Condition) 260 | 261 | // "update" action failing with unsatisfied EqualCondition (updateOwn preset). 262 | assert.Equal(s.T(), "update", permissionErrors[1].Action) 263 | assert.Equal(s.T(), 1, len(permissionErrors[1].ConditionErrors)) 264 | assert.IsType(s.T(), new(restrict.EqualCondition), permissionErrors[1].ConditionErrors[0].Condition) 265 | 266 | // "delete" action failing with unsatisfied EmptyCondition and greaterThanCondition. 267 | assert.Equal(s.T(), "delete", permissionErrors[2].Action) 268 | assert.Equal(s.T(), 2, len(permissionErrors[2].ConditionErrors)) 269 | assert.IsType(s.T(), new(restrict.EmptyCondition), permissionErrors[2].ConditionErrors[0].Condition) 270 | assert.IsType(s.T(), new(greaterThanCondition), permissionErrors[2].ConditionErrors[1].Condition) 271 | } 272 | -------------------------------------------------------------------------------- /condition_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | const marshalableConditionMockName = "TEST_CONDITION" 14 | const invalidMarshalableConditionMockName = "INVALID_TEST_CONDITION" 15 | 16 | type marshalableConditionMock struct { 17 | TestPropertyOne int `json:"testPropertyOne" yaml:"testPropertyOne"` 18 | TestPropertyTwo string `json:"testPropertyTwo" yaml:"testPropertyTwo"` 19 | } 20 | 21 | func (m *marshalableConditionMock) Type() string { 22 | return marshalableConditionMockName 23 | } 24 | 25 | func (m *marshalableConditionMock) Check(request *AccessRequest) error { 26 | return nil 27 | } 28 | 29 | type invalidMarshalableConditionMock struct { 30 | TestProperty int `json:"testProperty" yaml:"testProperty"` 31 | } 32 | 33 | func (m *invalidMarshalableConditionMock) Type() string { 34 | return invalidMarshalableConditionMockName 35 | } 36 | 37 | func (m *invalidMarshalableConditionMock) Check(request *AccessRequest) error { 38 | return nil 39 | } 40 | 41 | func (m *invalidMarshalableConditionMock) MarshalJSON() ([]byte, error) { 42 | return nil, errors.New("testError") 43 | } 44 | 45 | func (m *invalidMarshalableConditionMock) MarshalYAML() (interface{}, error) { 46 | return nil, errors.New("testError") 47 | } 48 | 49 | func (m *invalidMarshalableConditionMock) UnarshalJSON(jsonDate []byte) error { 50 | return errors.New("testError") 51 | } 52 | 53 | func (m *invalidMarshalableConditionMock) UnmarshalYAML(value *yaml.Node) error { 54 | return errors.New("testError") 55 | } 56 | 57 | type conditionsSuite struct { 58 | suite.Suite 59 | } 60 | 61 | func resetFactories() { 62 | ConditionFactories[marshalableConditionMockName] = nil 63 | ConditionFactories[invalidMarshalableConditionMockName] = nil 64 | } 65 | 66 | func TestConditionsSuite(t *testing.T) { 67 | suite.Run(t, new(conditionsSuite)) 68 | } 69 | 70 | func (s *conditionsSuite) BeforeTest(_, _ string) { 71 | //nolint 72 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 73 | return &marshalableConditionMock{} 74 | }) 75 | 76 | //nolint 77 | RegisterConditionFactory(invalidMarshalableConditionMockName, func() Condition { 78 | return &invalidMarshalableConditionMock{} 79 | }) 80 | } 81 | 82 | func (s *conditionsSuite) AfterTest(_, _ string) { 83 | resetFactories() 84 | } 85 | 86 | func (s *conditionsSuite) TestRegisterConditionFactory() { 87 | resetFactories() 88 | 89 | factory := func() Condition { 90 | return &marshalableConditionMock{} 91 | } 92 | 93 | err := RegisterConditionFactory(marshalableConditionMockName, factory) 94 | 95 | assert.Nil(s.T(), err) 96 | assert.NotNil(s.T(), ConditionFactories[marshalableConditionMockName]) 97 | 98 | err = RegisterConditionFactory(marshalableConditionMockName, factory) 99 | 100 | assert.IsType(s.T(), new(ConditionFactoryAlreadyExistsError), err) 101 | } 102 | 103 | func (s *conditionsSuite) TestUnmarshalJSON() { 104 | // Since ConditionsFactories is global, we make sure that the mocked Conditions's 105 | // factory is nil. 106 | ConditionFactories[marshalableConditionMockName] = nil 107 | 108 | conditionsData := []byte(`[ 109 | { 110 | "type": "TEST_CONDITION", 111 | "options": { 112 | "testPropertyOne": 1, 113 | "testPropertyTwo": "testString1" 114 | } 115 | }, 116 | { 117 | "type": "TEST_CONDITION", 118 | "options": { 119 | "testPropertyOne": 2, 120 | "testPropertyTwo": "testString2" 121 | } 122 | } 123 | ]`) 124 | 125 | assert.True(s.T(), json.Valid(conditionsData)) 126 | 127 | testConditions := Conditions{} 128 | 129 | err := testConditions.UnmarshalJSON(conditionsData) 130 | 131 | assert.IsType(s.T(), new(ConditionFactoryNotFoundError), err) 132 | 133 | //nolint 134 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 135 | return &marshalableConditionMock{} 136 | }) 137 | 138 | testConditions = Conditions{} 139 | 140 | err = testConditions.UnmarshalJSON(conditionsData) 141 | 142 | assert.Nil(s.T(), err) 143 | 144 | assert.IsType(s.T(), new(marshalableConditionMock), testConditions[0]) 145 | assert.IsType(s.T(), new(marshalableConditionMock), testConditions[1]) 146 | 147 | testConditionOne := testConditions[0].(*marshalableConditionMock) 148 | testConditionTwo := testConditions[1].(*marshalableConditionMock) 149 | 150 | assert.Equal(s.T(), 1, testConditionOne.TestPropertyOne) 151 | assert.Equal(s.T(), "testString1", testConditionOne.TestPropertyTwo) 152 | 153 | assert.Equal(s.T(), 2, testConditionTwo.TestPropertyOne) 154 | assert.Equal(s.T(), "testString2", testConditionTwo.TestPropertyTwo) 155 | } 156 | 157 | func (s *conditionsSuite) TestUnmarshalJSON_InvalidData() { 158 | //nolint 159 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 160 | return &marshalableConditionMock{} 161 | }) 162 | 163 | invalidConditionsData := []byte(`[ 164 | { 165 | type": "TEST_CONDITION" 166 | } 167 | ]`) 168 | 169 | testConditions := Conditions{} 170 | 171 | err := testConditions.UnmarshalJSON(invalidConditionsData) 172 | 173 | assert.Error(s.T(), err) 174 | 175 | // "testPropertyOne" has string instead of int. 176 | invalidConditionsData = []byte(`[ 177 | { 178 | "type": "TEST_CONDITION", 179 | "options": { 180 | "testPropertyOne": "2" 181 | } 182 | } 183 | ]`) 184 | 185 | err = json.Unmarshal(invalidConditionsData, &testConditions) 186 | 187 | assert.Error(s.T(), err) 188 | } 189 | 190 | func (s *conditionsSuite) TestUnmarshalYAML() { 191 | // Since ConditionsFactories is global, we make sure that the mocked Conditions's 192 | // factory is nil. 193 | ConditionFactories[marshalableConditionMockName] = nil 194 | 195 | // Note that one of the Conditions has empty type - UnmarshalYAML should omit 196 | // this Condition. 197 | conditionsData := []byte(` 198 | - type: TEST_CONDITION 199 | options: 200 | testPropertyOne: 1 201 | testPropertyTwo: "testString1" 202 | 203 | - type: TEST_CONDITION 204 | options: 205 | testPropertyOne: 2 206 | testPropertyTwo: "testString2" 207 | 208 | - type: 209 | options: 210 | testPropertyOne: 2 211 | testPropertyTwo: "testString2" 212 | `) 213 | 214 | testConditions := Conditions{} 215 | 216 | err := yaml.Unmarshal(conditionsData, &testConditions) 217 | 218 | assert.IsType(s.T(), new(ConditionFactoryNotFoundError), err) 219 | 220 | //nolint 221 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 222 | return &marshalableConditionMock{} 223 | }) 224 | 225 | err = yaml.Unmarshal(conditionsData, &testConditions) 226 | 227 | assert.Nil(s.T(), err) 228 | 229 | assert.IsType(s.T(), new(marshalableConditionMock), testConditions[0]) 230 | assert.IsType(s.T(), new(marshalableConditionMock), testConditions[1]) 231 | 232 | testConditionOne := testConditions[0].(*marshalableConditionMock) 233 | testConditionTwo := testConditions[1].(*marshalableConditionMock) 234 | 235 | assert.Equal(s.T(), 1, testConditionOne.TestPropertyOne) 236 | assert.Equal(s.T(), "testString1", testConditionOne.TestPropertyTwo) 237 | 238 | assert.Equal(s.T(), 2, testConditionTwo.TestPropertyOne) 239 | assert.Equal(s.T(), "testString2", testConditionTwo.TestPropertyTwo) 240 | } 241 | 242 | func (s *conditionsSuite) TestUnmarshalYAML_InvalidData() { 243 | // Missing hyphen at the beginning 244 | conditionsData := []byte(` 245 | - type: TEST_CONDITION 246 | options: 247 | testPropertyOne: "1" 248 | testPropertyTwo: "testString1" 249 | `) 250 | 251 | testConditions := Conditions{} 252 | err := yaml.Unmarshal(conditionsData, &testConditions) 253 | 254 | assert.Error(s.T(), err) 255 | } 256 | 257 | func (s *conditionsSuite) TestMarshalJSON() { 258 | //nolint 259 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 260 | return &marshalableConditionMock{} 261 | }) 262 | 263 | testConditionOne := &marshalableConditionMock{ 264 | TestPropertyOne: 1, 265 | TestPropertyTwo: "testString1", 266 | } 267 | 268 | testConditionTwo := &marshalableConditionMock{ 269 | TestPropertyOne: 2, 270 | TestPropertyTwo: "testString2", 271 | } 272 | 273 | testConditions := Conditions{ 274 | testConditionOne, 275 | testConditionTwo, 276 | } 277 | 278 | conditionsJSON, err := testConditions.MarshalJSON() 279 | 280 | assert.Nil(s.T(), err) 281 | assert.True(s.T(), json.Valid(conditionsJSON)) 282 | 283 | testConditions = Conditions{} 284 | 285 | err = testConditions.UnmarshalJSON(conditionsJSON) 286 | 287 | assert.Nil(s.T(), err) 288 | 289 | testConditionOne = testConditions[0].(*marshalableConditionMock) 290 | testConditionTwo = testConditions[1].(*marshalableConditionMock) 291 | 292 | assert.Equal(s.T(), 1, testConditionOne.TestPropertyOne) 293 | assert.Equal(s.T(), "testString1", testConditionOne.TestPropertyTwo) 294 | 295 | assert.Equal(s.T(), 2, testConditionTwo.TestPropertyOne) 296 | assert.Equal(s.T(), "testString2", testConditionTwo.TestPropertyTwo) 297 | } 298 | 299 | func (s *conditionsSuite) TestMarshalJSON_InvalidCondition() { 300 | testCondition := &invalidMarshalableConditionMock{ 301 | TestProperty: 1, 302 | } 303 | 304 | testConditions := Conditions{ 305 | testCondition, 306 | } 307 | 308 | conditionsJSON, err := testConditions.MarshalJSON() 309 | 310 | assert.Nil(s.T(), conditionsJSON) 311 | assert.Error(s.T(), err) 312 | } 313 | 314 | func (s *conditionsSuite) TestMarshalYAML() { 315 | //nolint 316 | RegisterConditionFactory(marshalableConditionMockName, func() Condition { 317 | return &marshalableConditionMock{} 318 | }) 319 | 320 | testConditionOne := &marshalableConditionMock{ 321 | TestPropertyOne: 1, 322 | TestPropertyTwo: "testString1", 323 | } 324 | 325 | testConditionTwo := &marshalableConditionMock{ 326 | TestPropertyOne: 2, 327 | TestPropertyTwo: "testString2", 328 | } 329 | 330 | testConditions := Conditions{ 331 | testConditionOne, 332 | testConditionTwo, 333 | } 334 | 335 | conditionsYAML, err := yaml.Marshal(testConditions) 336 | 337 | assert.Nil(s.T(), err) 338 | 339 | testConditions = Conditions{} 340 | 341 | err = yaml.Unmarshal(conditionsYAML, &testConditions) 342 | 343 | assert.Nil(s.T(), err) 344 | 345 | testConditionOne = testConditions[0].(*marshalableConditionMock) 346 | testConditionTwo = testConditions[1].(*marshalableConditionMock) 347 | 348 | assert.Equal(s.T(), 1, testConditionOne.TestPropertyOne) 349 | assert.Equal(s.T(), "testString1", testConditionOne.TestPropertyTwo) 350 | 351 | assert.Equal(s.T(), 2, testConditionTwo.TestPropertyOne) 352 | assert.Equal(s.T(), "testString2", testConditionTwo.TestPropertyTwo) 353 | } 354 | 355 | func (s *conditionsSuite) TestMarshalYAML_InvalidCondition() { 356 | testCondition := &invalidMarshalableConditionMock{ 357 | TestProperty: 1, 358 | } 359 | 360 | testConditions := Conditions{ 361 | testCondition, 362 | } 363 | 364 | conditionsYAML, err := yaml.Marshal(testConditions) 365 | 366 | assert.Nil(s.T(), conditionsYAML) 367 | assert.Error(s.T(), err) 368 | } 369 | 370 | func (s *conditionsSuite) TestFactories() { 371 | equalConditionFactory := ConditionFactories[EqualConditionType] 372 | notEqualConditionFactory := ConditionFactories[NotEqualConditionType] 373 | emptyConditionFactory := ConditionFactories[EmptyConditionType] 374 | notEmptyConditionFactory := ConditionFactories[NotEmptyConditionType] 375 | 376 | assert.IsType(s.T(), new(EqualCondition), equalConditionFactory()) 377 | assert.IsType(s.T(), new(NotEqualCondition), notEqualConditionFactory()) 378 | assert.IsType(s.T(), new(EmptyCondition), emptyConditionFactory()) 379 | assert.IsType(s.T(), new(NotEmptyCondition), notEmptyConditionFactory()) 380 | } 381 | -------------------------------------------------------------------------------- /policy_manager.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import "sync" 4 | 5 | // PolicyManager - an entity responsible for managing PolicyDefinition. It uses passed StorageAdapter 6 | // for policy persistence. 7 | type PolicyManager struct { 8 | // StorageAdapter used to load and save policy. 9 | adapter StorageAdapter 10 | 11 | // If set to true, PolicyManager will use it's StorageAdapter to save 12 | // the policy every time any change is made. 13 | autoUpdate bool 14 | 15 | // PolicyDefinition currently loaded into memory. 16 | policy *PolicyDefinition 17 | 18 | // PolicyManager should thread-safe for writing operations, therefore it uses RWMutex. 19 | sync.RWMutex 20 | } 21 | 22 | // NewPolicyManager - returns new PolicyManager instance and loads PolicyDefinition 23 | // using passed StorageAdapter. 24 | func NewPolicyManager(adapter StorageAdapter, autoUpdate bool) (*PolicyManager, error) { 25 | manager := &PolicyManager{ 26 | adapter: adapter, 27 | autoUpdate: autoUpdate, 28 | } 29 | 30 | // Load and initialize the policy. 31 | if err := manager.LoadPolicy(); err != nil { 32 | return nil, err 33 | } 34 | 35 | return manager, nil 36 | } 37 | 38 | // LoadPolicy - proxy method for loading the policy via StorageAdapter set 39 | // when creating PolicyManager instance. 40 | // Calling this method will override currently loaded policy. 41 | func (pm *PolicyManager) LoadPolicy() error { 42 | pm.Lock() 43 | defer pm.Unlock() 44 | 45 | policy, err := pm.adapter.LoadPolicy() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | pm.policy = policy 51 | 52 | if err := pm.applyPresets(); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // SavePolicy - proxy method for saving the policy via StorageAdapter set 60 | // when creating PolicyManager instance. 61 | func (pm *PolicyManager) SavePolicy() error { 62 | return pm.adapter.SavePolicy(pm.policy) 63 | } 64 | 65 | // GetPolicy - returns currently loaded PolicyDefinition. 66 | func (pm *PolicyManager) GetPolicy() *PolicyDefinition { 67 | pm.RLock() 68 | defer pm.RUnlock() 69 | 70 | return pm.policy 71 | } 72 | 73 | // applyPresets - applies defined presets to Permissions that are not yet merged. 74 | func (pm *PolicyManager) applyPresets() error { 75 | // For every Role, iterate over all Permissions for given Resource and 76 | // merge Permission with it's preset if defined. 77 | for _, role := range pm.policy.Roles { 78 | for _, grants := range role.Grants { 79 | for _, permission := range grants { 80 | if permission.Preset != "" { 81 | if err := pm.applyPreset(permission); err != nil { 82 | return err 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // applyPreset - applies defined preset to Permission. 93 | func (pm *PolicyManager) applyPreset(permission *Permission) error { 94 | preset := pm.policy.PermissionPresets[permission.Preset] 95 | 96 | // If given preset does not exist, return an error. 97 | if preset == nil { 98 | return newPermissionPresetNotFoundError(permission.Preset) 99 | } 100 | 101 | // Otherwise, merge found preset into Permission. 102 | permission.mergePreset(preset) 103 | 104 | return nil 105 | } 106 | 107 | // GetRole - returns a Role with given ID from currently loaded PolicyDefiniton. 108 | func (pm *PolicyManager) GetRole(roleID string) (*Role, error) { 109 | pm.RLock() 110 | defer pm.RUnlock() 111 | 112 | role := pm.getRole(roleID) 113 | // If given Role does not exists, return an error. 114 | if role == nil { 115 | return nil, newRoleNotFoundError(roleID) 116 | } 117 | 118 | return role, nil 119 | } 120 | 121 | // AddRole - adds a new role to the policy. 122 | // Saves with StorageAdapter if autoUpdate is set to true. 123 | func (pm *PolicyManager) AddRole(role *Role) error { 124 | pm.Lock() 125 | defer pm.Unlock() 126 | 127 | // Check if role already exists - if yes, return an error. 128 | if r := pm.getRole(role.ID); r != nil { 129 | return newRoleAlreadyExistsError(role.ID) 130 | } 131 | 132 | pm.policy.Roles[role.ID] = role 133 | 134 | // Since new Permissions with presets could be added, run ApplyPresets. 135 | if err := pm.applyPresets(); err != nil { 136 | return err 137 | } 138 | 139 | if pm.autoUpdate { 140 | return pm.adapter.SavePolicy(pm.policy) 141 | } 142 | 143 | return nil 144 | } 145 | 146 | // UpdateRole - updates existing Role in currently loaded policy. 147 | // Saves with StorageAdapter if autoUpdate is set to true. 148 | func (pm *PolicyManager) UpdateRole(role *Role) error { 149 | pm.Lock() 150 | defer pm.Unlock() 151 | 152 | // If given Role does not exists, return an error. 153 | if r := pm.getRole(role.ID); r == nil { 154 | return newRoleNotFoundError(role.ID) 155 | } 156 | 157 | pm.policy.Roles[role.ID] = role 158 | 159 | // Since new Permissions with presets could be added, run ApplyPresets. 160 | if err := pm.applyPresets(); err != nil { 161 | return err 162 | } 163 | 164 | if pm.autoUpdate { 165 | return pm.adapter.SavePolicy(pm.policy) 166 | } 167 | 168 | return nil 169 | } 170 | 171 | // UpsertRole - updates a Role if exists, adds new Role otherwise. 172 | // Saves with StorageAdapter if autoUpdate is set to true. 173 | func (pm *PolicyManager) UpsertRole(role *Role) error { 174 | if err := pm.UpdateRole(role); err != nil { 175 | if _, ok := err.(*RoleNotFoundError); ok { 176 | return pm.AddRole(role) 177 | } 178 | 179 | return err 180 | } 181 | 182 | return nil 183 | } 184 | 185 | // DeleteRole - removes a Role with given ID. 186 | // Saves with StorageAdapter if autoUpdate is set to true. 187 | func (pm *PolicyManager) DeleteRole(roleID string) error { 188 | pm.Lock() 189 | defer pm.Unlock() 190 | 191 | if pm.policy.Roles == nil { 192 | pm.policy.Roles = Roles{} 193 | } 194 | 195 | // If Role with given ID does not exist, return an error. 196 | if r := pm.getRole(roleID); r == nil { 197 | return newRoleNotFoundError(roleID) 198 | } 199 | 200 | delete(pm.policy.Roles, roleID) 201 | 202 | if pm.autoUpdate { 203 | return pm.adapter.SavePolicy(pm.policy) 204 | } 205 | 206 | return nil 207 | } 208 | 209 | // AddPermission - adds a new Permission for the Role and Resource with passed ids. 210 | // Saves with StorageAdapter if autoUpdate is set to true. 211 | func (pm *PolicyManager) AddPermission(roleID, resourceID string, permission *Permission) error { 212 | pm.Lock() 213 | defer pm.Unlock() 214 | 215 | role := pm.getRole(roleID) 216 | // If role does not exist, return an error. 217 | if role == nil { 218 | return newRoleNotFoundError(roleID) 219 | } 220 | 221 | pm.ensurePermissionsArray(role, resourceID) 222 | 223 | role.Grants[resourceID] = append(role.Grants[resourceID], permission) 224 | 225 | // If added Permission has preset defined, apply it immediately. 226 | if permission.Preset != "" { 227 | if err := pm.applyPreset(permission); err != nil { 228 | return err 229 | } 230 | } 231 | 232 | if pm.autoUpdate { 233 | return pm.adapter.SavePolicy(pm.policy) 234 | } 235 | 236 | return nil 237 | } 238 | 239 | // DeletePermission - removes a Permission with given name for Role and Resource with 240 | // passed ids. Please note that deleting a Permission for given action will revoke 241 | // ALL of the Permissions that share this action. 242 | // Saves with StorageAdapter if autoUpdate is set to true. 243 | func (pm *PolicyManager) DeletePermission(roleID, resourceID, action string) error { 244 | pm.Lock() 245 | defer pm.Unlock() 246 | 247 | role := pm.getRole(roleID) 248 | 249 | // If role does not exist, return an error. 250 | if role == nil { 251 | return newRoleNotFoundError(roleID) 252 | } 253 | 254 | pm.ensurePermissionsArray(role, resourceID) 255 | 256 | for i, permission := range role.Grants[resourceID] { 257 | if permission.Action == action { 258 | role.Grants[resourceID] = pm.deletePermissionFromSlice(role.Grants[resourceID], i) 259 | } 260 | } 261 | 262 | if pm.autoUpdate { 263 | return pm.adapter.SavePolicy(pm.policy) 264 | } 265 | 266 | return nil 267 | } 268 | 269 | // deletePermissionFromSlice - helper function for removing Permission under given index 270 | // from Permissions slice. 271 | func (pm *PolicyManager) deletePermissionFromSlice(grants []*Permission, index int) []*Permission { 272 | if index >= 0 { 273 | newGrants := make([]*Permission, 0) 274 | newGrants = append(newGrants, grants[:index]...) 275 | newGrants = append(newGrants, grants[index+1:]...) 276 | 277 | return newGrants 278 | } 279 | 280 | return grants 281 | } 282 | 283 | // AddPermissionPreset - adds new Permission preset to PolicyDefinition. 284 | // Saves with StorageAdapter if autoUpdate is set to true. 285 | func (pm *PolicyManager) AddPermissionPreset(name string, preset *Permission) error { 286 | pm.Lock() 287 | defer pm.Unlock() 288 | 289 | // If there is already a preset with given name, return an error. 290 | if p := pm.getPermissionPreset(name); p != nil { 291 | return newPermissionPresetAlreadyExistsError(name) 292 | } 293 | 294 | if pm.policy.PermissionPresets == nil { 295 | pm.policy.PermissionPresets = PermissionPresets{} 296 | } 297 | 298 | pm.policy.PermissionPresets[name] = preset 299 | 300 | if pm.autoUpdate { 301 | return pm.adapter.SavePolicy(pm.policy) 302 | } 303 | 304 | return nil 305 | } 306 | 307 | // UpdatePermissionPreset - updates a Permission preset in PolicyDefinition. 308 | // Saves with StorageAdapter if autoUpdate is set to true. 309 | func (pm *PolicyManager) UpdatePermissionPreset(name string, preset *Permission) error { 310 | pm.Lock() 311 | defer pm.Unlock() 312 | 313 | // If there is no preset with given name, return an error. 314 | if p := pm.getPermissionPreset(name); p == nil { 315 | return newPermissionPresetNotFoundError(name) 316 | } 317 | 318 | pm.policy.PermissionPresets[name] = preset 319 | 320 | if pm.autoUpdate { 321 | return pm.adapter.SavePolicy(pm.policy) 322 | } 323 | 324 | return nil 325 | } 326 | 327 | // UpsertPermissionPreset - updates Permission preset if exists, adds a new otherwise. 328 | // Saves with StorageAdapter if autoUpdate is set to true. 329 | func (pm *PolicyManager) UpsertPermissionPreset(name string, preset *Permission) error { 330 | if err := pm.UpdatePermissionPreset(name, preset); err != nil { 331 | if _, ok := err.(*PermissionPresetNotFoundError); ok { 332 | return pm.AddPermissionPreset(name, preset) 333 | } 334 | 335 | return err 336 | } 337 | 338 | return nil 339 | } 340 | 341 | // DeletePermissionPreset - removes Permission preset with given name. 342 | // Saves with StorageAdapter if autoUpdate is set to true. 343 | func (pm *PolicyManager) DeletePermissionPreset(name string) error { 344 | pm.Lock() 345 | defer pm.Unlock() 346 | 347 | // If there is no preset with given name, return an error. 348 | if p := pm.getPermissionPreset(name); p == nil { 349 | return newPermissionPresetNotFoundError(name) 350 | } 351 | 352 | delete(pm.policy.PermissionPresets, name) 353 | 354 | if pm.autoUpdate { 355 | return pm.adapter.SavePolicy(pm.policy) 356 | } 357 | 358 | return nil 359 | } 360 | 361 | // DisableAutoUpdate - disables automatic update. 362 | func (pm *PolicyManager) DisableAutoUpdate() { 363 | pm.autoUpdate = false 364 | } 365 | 366 | // EnableAutoUpdate - enables automatic update. 367 | func (pm *PolicyManager) EnableAutoUpdate() { 368 | pm.autoUpdate = true 369 | } 370 | 371 | // ensurePermissionsArray - helper function for setting GrantsMap and Permissions array 372 | // for given Role if they don't exist (i.e. are equal to nil). 373 | func (pm *PolicyManager) ensurePermissionsArray(role *Role, resourceID string) { 374 | if role.Grants == nil { 375 | role.Grants = GrantsMap{} 376 | } 377 | 378 | if role.Grants[resourceID] == nil { 379 | role.Grants[resourceID] = []*Permission{} 380 | } 381 | } 382 | 383 | // getRole - helper function for getting a Role with given ID. 384 | func (pm *PolicyManager) getRole(roleID string) *Role { 385 | role, ok := pm.policy.Roles[roleID] 386 | 387 | if !ok { 388 | return nil 389 | } 390 | 391 | return role 392 | } 393 | 394 | // getPermissionPreset - helper function for getting PermissionPreset from PolicyDefinition. 395 | func (pm *PolicyManager) getPermissionPreset(name string) *Permission { 396 | preset, ok := pm.policy.PermissionPresets[name] 397 | 398 | if !ok { 399 | return nil 400 | } 401 | 402 | return preset 403 | } 404 | -------------------------------------------------------------------------------- /adapters/file_adapter_test.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/el-mike/restrict/v2" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type fileHandlerMock struct { 14 | mock.Mock 15 | } 16 | 17 | func (m *fileHandlerMock) ReadFile(name string) ([]byte, error) { 18 | args := m.Called(name) 19 | 20 | if args.Get(0) == nil { 21 | return nil, args.Error(1) 22 | } 23 | 24 | return args.Get(0).([]byte), args.Error(1) 25 | } 26 | 27 | func (m *fileHandlerMock) WriteFile(name string, data []byte, perm FilePerm) error { 28 | args := m.Called(name, data, perm) 29 | 30 | return args.Error(0) 31 | } 32 | 33 | type jsonHandlerMock struct { 34 | mock.Mock 35 | } 36 | 37 | func (m *jsonHandlerMock) Unmarshal(data []byte, v interface{}) error { 38 | args := m.Called(data, v) 39 | 40 | return args.Error(0) 41 | } 42 | 43 | func (m *jsonHandlerMock) MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { 44 | args := m.Called(v, prefix, indent) 45 | 46 | if args.Get(0) == nil { 47 | return nil, args.Error(1) 48 | } 49 | 50 | return args.Get(0).([]byte), args.Error(1) 51 | } 52 | 53 | type yamlHandlerMock struct { 54 | mock.Mock 55 | } 56 | 57 | func (m *yamlHandlerMock) Unmarshal(in []byte, out interface{}) error { 58 | args := m.Called(in, out) 59 | 60 | return args.Error(0) 61 | } 62 | 63 | func (m *yamlHandlerMock) Marshal(in interface{}) ([]byte, error) { 64 | args := m.Called(in) 65 | 66 | if args.Get(0) == nil { 67 | return nil, args.Error(1) 68 | } 69 | 70 | return args.Get(0).([]byte), args.Error(1) 71 | } 72 | 73 | type fileAdapterSuite struct { 74 | suite.Suite 75 | 76 | testFileName string 77 | testError error 78 | } 79 | 80 | func (s *fileAdapterSuite) SetupSuite() { 81 | s.testFileName = "testFile.json" 82 | s.testError = errors.New("testError") 83 | } 84 | 85 | func TestFileAdapterSuite(t *testing.T) { 86 | suite.Run(t, new(fileAdapterSuite)) 87 | } 88 | 89 | func (s *fileAdapterSuite) TestNewFileAdapter() { 90 | testFileType := JSONFile 91 | 92 | adapter := NewFileAdapter(s.testFileName, testFileType) 93 | 94 | assert.NotNil(s.T(), adapter) 95 | assert.Equal(s.T(), adapter.fileName, s.testFileName) 96 | assert.Equal(s.T(), adapter.fileType, testFileType) 97 | 98 | assert.IsType(s.T(), adapter.fileHandler, new(defaultFileHandler)) 99 | assert.IsType(s.T(), adapter.jsonHandler, new(defaultJSONHandler)) 100 | assert.IsType(s.T(), adapter.yamlHandler, new(defaultYAMLHandler)) 101 | 102 | testFileType = YAMLFile 103 | 104 | adapter = NewFileAdapter(s.testFileName, testFileType) 105 | 106 | assert.NotNil(s.T(), adapter) 107 | assert.Equal(s.T(), adapter.fileType, testFileType) 108 | } 109 | 110 | func (s *fileAdapterSuite) TestSetJSONIndent() { 111 | adapter := NewFileAdapter(s.testFileName, JSONFile) 112 | 113 | assert.Equal(s.T(), adapter.jsonIndent, defaultJSONIndent) 114 | 115 | testIndent := " " 116 | 117 | adapter.SetJSONIndent(testIndent) 118 | 119 | assert.Equal(s.T(), adapter.jsonIndent, testIndent) 120 | } 121 | 122 | func (s *fileAdapterSuite) TestSetFilePerm() { 123 | adapter := NewFileAdapter(s.testFileName, JSONFile) 124 | 125 | assert.Equal(s.T(), adapter.filePerm, defaultFilePerm) 126 | 127 | testPerm := FilePerm(0777) 128 | 129 | adapter.SetFilePerm(testPerm) 130 | 131 | assert.Equal(s.T(), adapter.filePerm, testPerm) 132 | } 133 | 134 | func (s *fileAdapterSuite) TestLoadPolicy_ReadFile() { 135 | // Load with working fileHadler. 136 | testData := []byte("{}") 137 | 138 | workingFileHandler := new(fileHandlerMock) 139 | workingFileHandler.On( 140 | "ReadFile", 141 | mock.Anything, 142 | ).Return(testData, nil) 143 | 144 | adapter := NewFileAdapter(s.testFileName, JSONFile) 145 | 146 | adapter.fileHandler = workingFileHandler 147 | 148 | _, err := adapter.LoadPolicy() 149 | 150 | assert.Nil(s.T(), err) 151 | workingFileHandler.AssertNumberOfCalls(s.T(), "ReadFile", 1) 152 | workingFileHandler.AssertCalled(s.T(), "ReadFile", s.testFileName) 153 | 154 | // Load with incorrect fileType 155 | adapter.fileType = "incorrectFileType" 156 | _, err = adapter.LoadPolicy() 157 | 158 | assert.Error(s.T(), err) 159 | assert.IsType(s.T(), new(FileTypeNotSupportedError), err) 160 | 161 | // Load with failing fileHandler 162 | 163 | failingFileHandler := new(fileHandlerMock) 164 | failingFileHandler.On( 165 | "ReadFile", 166 | mock.Anything, 167 | ).Return(nil, s.testError) 168 | 169 | adapter.fileHandler = failingFileHandler 170 | 171 | _, err = adapter.LoadPolicy() 172 | 173 | assert.Error(s.T(), err) 174 | assert.Equal(s.T(), err, s.testError) 175 | } 176 | 177 | func (s *fileAdapterSuite) TestLoadPolicy_JSONFile() { 178 | // Load with working jsonHandler 179 | testData := []byte(getBasicPolicyJSONString()) 180 | 181 | testFileHandler := new(fileHandlerMock) 182 | testFileHandler.On( 183 | "ReadFile", 184 | mock.Anything, 185 | ).Return(testData, nil) 186 | 187 | workingJSONHandler := new(jsonHandlerMock) 188 | workingJSONHandler.On( 189 | "Unmarshal", 190 | mock.Anything, 191 | mock.Anything, 192 | ).Return(nil) 193 | 194 | workingYAMLHandler := new(yamlHandlerMock) 195 | workingYAMLHandler.On( 196 | "Marshal", 197 | mock.Anything, 198 | ).Return(testData, nil) 199 | 200 | adapter := NewFileAdapter(s.testFileName, JSONFile) 201 | 202 | adapter.fileHandler = testFileHandler 203 | adapter.jsonHandler = workingJSONHandler 204 | 205 | policy, err := adapter.LoadPolicy() 206 | 207 | assert.Nil(s.T(), err) 208 | assert.IsType(s.T(), policy, new(restrict.PolicyDefinition)) 209 | workingJSONHandler.AssertNumberOfCalls(s.T(), "Unmarshal", 1) 210 | workingJSONHandler.AssertCalled(s.T(), "Unmarshal", testData, mock.Anything) 211 | 212 | // Load with failing jsonHandler 213 | failingJSONHandler := new(jsonHandlerMock) 214 | failingJSONHandler.On( 215 | "Unmarshal", 216 | mock.Anything, 217 | mock.Anything, 218 | ).Return(s.testError) 219 | 220 | adapter.jsonHandler = failingJSONHandler 221 | 222 | policy, err = adapter.LoadPolicy() 223 | 224 | assert.Nil(s.T(), policy) 225 | assert.Error(s.T(), err) 226 | } 227 | 228 | func (s *fileAdapterSuite) TestLoadPolicy_JSONReal() { 229 | testData := []byte(getBasicPolicyJSONString()) 230 | 231 | testFileHandler := new(fileHandlerMock) 232 | testFileHandler.On( 233 | "ReadFile", 234 | mock.Anything, 235 | ).Return(testData, nil) 236 | 237 | adapter := NewFileAdapter("test.json", JSONFile) 238 | 239 | adapter.fileHandler = testFileHandler 240 | 241 | policy, err := adapter.LoadPolicy() 242 | 243 | assert.Nil(s.T(), err) 244 | assert.Equal(s.T(), 1, len(policy.Roles)) 245 | } 246 | 247 | func (s *fileAdapterSuite) TestLoadPolicy_YAMLFile() { 248 | // Load with working yamlHandler 249 | testData := []byte(getBasicPolicyYAMLString()) 250 | 251 | testFileHandler := new(fileHandlerMock) 252 | testFileHandler.On( 253 | "ReadFile", 254 | mock.Anything, 255 | ).Return(testData, nil) 256 | 257 | workingYAMLHandler := new(yamlHandlerMock) 258 | workingYAMLHandler.On( 259 | "Unmarshal", 260 | mock.Anything, 261 | mock.Anything, 262 | ).Return(nil) 263 | 264 | adapter := NewFileAdapter(s.testFileName, YAMLFile) 265 | 266 | adapter.fileHandler = testFileHandler 267 | adapter.yamlHandler = workingYAMLHandler 268 | 269 | policy, err := adapter.LoadPolicy() 270 | 271 | assert.Nil(s.T(), err) 272 | assert.IsType(s.T(), policy, new(restrict.PolicyDefinition)) 273 | workingYAMLHandler.AssertNumberOfCalls(s.T(), "Unmarshal", 1) 274 | workingYAMLHandler.AssertCalled(s.T(), "Unmarshal", testData, mock.Anything) 275 | 276 | // Load with failing jsonHandler 277 | failingYAMLHandler := new(yamlHandlerMock) 278 | failingYAMLHandler.On( 279 | "Unmarshal", 280 | mock.Anything, 281 | mock.Anything, 282 | ).Return(s.testError) 283 | 284 | adapter.yamlHandler = failingYAMLHandler 285 | 286 | policy, err = adapter.LoadPolicy() 287 | 288 | assert.Nil(s.T(), policy) 289 | assert.Error(s.T(), err) 290 | } 291 | 292 | func (s *fileAdapterSuite) TestLoadPolicy_YAMLReal() { 293 | testData := []byte(getBasicPolicyYAMLString()) 294 | 295 | testFileHandler := new(fileHandlerMock) 296 | testFileHandler.On( 297 | "ReadFile", 298 | mock.Anything, 299 | ).Return(testData, nil) 300 | 301 | adapter := NewFileAdapter("test.yml", YAMLFile) 302 | 303 | adapter.fileHandler = testFileHandler 304 | 305 | policy, err := adapter.LoadPolicy() 306 | 307 | assert.Nil(s.T(), err) 308 | assert.Equal(s.T(), 1, len(policy.Roles)) 309 | } 310 | 311 | func (s *fileAdapterSuite) TestSavePolicy() { 312 | testPolicy := getBasicPolicy() 313 | 314 | adapter := NewFileAdapter(s.testFileName, "incorrectFileType") 315 | 316 | err := adapter.SavePolicy(testPolicy) 317 | 318 | assert.NotNil(s.T(), err) 319 | assert.IsType(s.T(), err, new(FileTypeNotSupportedError)) 320 | } 321 | 322 | func (s *fileAdapterSuite) TestSavePolicy_WriteFile() { 323 | // Write with working fileHandler 324 | testJSONData := []byte(getBasicPolicyJSONString()) 325 | testYAMLData := []byte(getBasicPolicyYAMLString()) 326 | testPolicy := getBasicPolicy() 327 | 328 | workingFileHandler := new(fileHandlerMock) 329 | workingFileHandler.On( 330 | "WriteFile", 331 | mock.Anything, 332 | mock.Anything, 333 | mock.Anything, 334 | ).Return(nil) 335 | 336 | workingJSONHandler := new(jsonHandlerMock) 337 | workingJSONHandler.On( 338 | "MarshalIndent", 339 | mock.Anything, 340 | mock.Anything, 341 | mock.Anything, 342 | ).Return(testJSONData, nil) 343 | 344 | workingYAMLHandler := new(yamlHandlerMock) 345 | workingYAMLHandler.On( 346 | "Marshal", 347 | mock.Anything, 348 | ).Return(testYAMLData, nil) 349 | 350 | adapter := NewFileAdapter(s.testFileName, JSONFile) 351 | 352 | adapter.fileHandler = workingFileHandler 353 | adapter.jsonHandler = workingJSONHandler 354 | 355 | err := adapter.SavePolicy(testPolicy) 356 | 357 | assert.Nil(s.T(), err) 358 | workingFileHandler.AssertNumberOfCalls(s.T(), "WriteFile", 1) 359 | workingFileHandler.AssertCalled(s.T(), "WriteFile", s.testFileName, mock.Anything, defaultFilePerm) 360 | 361 | // Write with failing fileHandler for JSON 362 | failingFileHandler := new(fileHandlerMock) 363 | failingFileHandler.On( 364 | "WriteFile", 365 | mock.Anything, 366 | mock.Anything, 367 | mock.Anything, 368 | ).Return(s.testError) 369 | 370 | adapter.fileHandler = failingFileHandler 371 | 372 | err = adapter.SavePolicy(testPolicy) 373 | 374 | assert.Error(s.T(), err) 375 | assert.Equal(s.T(), err, s.testError) 376 | 377 | // Write with failing fileHandler for YAML 378 | adapter = NewFileAdapter(s.testFileName, YAMLFile) 379 | 380 | adapter.fileHandler = failingFileHandler 381 | adapter.yamlHandler = workingYAMLHandler 382 | 383 | err = adapter.SavePolicy(testPolicy) 384 | 385 | assert.Error(s.T(), err) 386 | assert.Equal(s.T(), err, s.testError) 387 | } 388 | 389 | func (s *fileAdapterSuite) TestSavePolicy_JSONFile() { 390 | // Save with working jsonHandler 391 | testData := []byte(getBasicPolicyJSONString()) 392 | testPolicy := getBasicPolicy() 393 | 394 | workingFileHandler := new(fileHandlerMock) 395 | workingFileHandler.On( 396 | "WriteFile", 397 | mock.Anything, 398 | mock.Anything, 399 | mock.Anything, 400 | ).Return(nil) 401 | 402 | workingJSONHandler := new(jsonHandlerMock) 403 | workingJSONHandler.On( 404 | "MarshalIndent", 405 | mock.Anything, 406 | mock.Anything, 407 | mock.Anything, 408 | ).Return(testData, nil) 409 | 410 | adapter := NewFileAdapter(s.testFileName, JSONFile) 411 | 412 | adapter.fileHandler = workingFileHandler 413 | adapter.jsonHandler = workingJSONHandler 414 | 415 | err := adapter.SavePolicy(testPolicy) 416 | 417 | assert.Nil(s.T(), err) 418 | workingJSONHandler.AssertNumberOfCalls(s.T(), "MarshalIndent", 1) 419 | workingJSONHandler.AssertCalled(s.T(), "MarshalIndent", testPolicy, "", defaultJSONIndent) 420 | 421 | workingFileHandler.AssertNumberOfCalls(s.T(), "WriteFile", 1) 422 | workingFileHandler.AssertCalled(s.T(), "WriteFile", s.testFileName, testData, defaultFilePerm) 423 | 424 | // Save with failing jsonHandler 425 | failingJSONHandler := new(jsonHandlerMock) 426 | failingJSONHandler.On( 427 | "MarshalIndent", 428 | mock.Anything, 429 | mock.Anything, 430 | mock.Anything, 431 | ).Return([]byte{}, s.testError) 432 | 433 | adapter.jsonHandler = failingJSONHandler 434 | 435 | err = adapter.SavePolicy(testPolicy) 436 | 437 | assert.Error(s.T(), err) 438 | assert.Equal(s.T(), err, s.testError) 439 | 440 | failingJSONHandler.AssertNumberOfCalls(s.T(), "MarshalIndent", 1) 441 | // Since we are reusing workingFileHandler, 1 means that it was not called 442 | // again if jsonHandler returned error. 443 | workingFileHandler.AssertNumberOfCalls(s.T(), "WriteFile", 1) 444 | } 445 | 446 | func (s *fileAdapterSuite) TestSavePolicy_YAMLFile() { 447 | // Save with working yamlHandler 448 | testData := []byte(getBasicPolicyYAMLString()) 449 | testPolicy := getBasicPolicy() 450 | 451 | workingFileHandler := new(fileHandlerMock) 452 | workingFileHandler.On( 453 | "WriteFile", 454 | mock.Anything, 455 | mock.Anything, 456 | mock.Anything, 457 | ).Return(nil) 458 | 459 | workingYAMLHandler := new(yamlHandlerMock) 460 | workingYAMLHandler.On( 461 | "Marshal", 462 | mock.Anything, 463 | ).Return(testData, nil) 464 | 465 | adapter := NewFileAdapter(s.testFileName, YAMLFile) 466 | 467 | adapter.fileHandler = workingFileHandler 468 | adapter.yamlHandler = workingYAMLHandler 469 | 470 | err := adapter.SavePolicy(testPolicy) 471 | 472 | assert.Nil(s.T(), err) 473 | workingYAMLHandler.AssertNumberOfCalls(s.T(), "Marshal", 1) 474 | workingYAMLHandler.AssertCalled(s.T(), "Marshal", testPolicy) 475 | 476 | workingFileHandler.AssertNumberOfCalls(s.T(), "WriteFile", 1) 477 | workingFileHandler.AssertCalled(s.T(), "WriteFile", s.testFileName, testData, defaultFilePerm) 478 | 479 | // Save with failing yamlHandler 480 | failingYAMLHandler := new(yamlHandlerMock) 481 | failingYAMLHandler.On( 482 | "Marshal", 483 | mock.Anything, 484 | ).Return([]byte{}, s.testError) 485 | 486 | adapter.yamlHandler = failingYAMLHandler 487 | 488 | err = adapter.SavePolicy(testPolicy) 489 | 490 | assert.Error(s.T(), err) 491 | assert.Equal(s.T(), err, s.testError) 492 | 493 | failingYAMLHandler.AssertNumberOfCalls(s.T(), "Marshal", 1) 494 | // Since we are reusing workingFileHandler, 1 means that it was not called 495 | // again if yamlHandler returned error. 496 | workingFileHandler.AssertNumberOfCalls(s.T(), "WriteFile", 1) 497 | } 498 | -------------------------------------------------------------------------------- /policy_manager_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type storageAdapterMock struct { 13 | mock.Mock 14 | } 15 | 16 | func (m *storageAdapterMock) LoadPolicy() (*PolicyDefinition, error) { 17 | args := m.Called() 18 | 19 | if args.Get(0) == nil { 20 | return nil, args.Error(1) 21 | } 22 | 23 | return args.Get(0).(*PolicyDefinition), args.Error(1) 24 | } 25 | 26 | func (m *storageAdapterMock) SavePolicy(policy *PolicyDefinition) error { 27 | args := m.Called(policy) 28 | 29 | return args.Error(0) 30 | } 31 | 32 | type policyManagerSuite struct { 33 | suite.Suite 34 | 35 | testError error 36 | } 37 | 38 | func (s *policyManagerSuite) SetupSuite() { 39 | s.testError = errors.New("testError") 40 | } 41 | 42 | func TestPolicyManagerSuite(t *testing.T) { 43 | suite.Run(t, new(policyManagerSuite)) 44 | } 45 | 46 | func (s *policyManagerSuite) TestNewPolicyManager() { 47 | testPolicy := getBasicPolicy() 48 | 49 | // Failing adapter 50 | testAdapter := new(storageAdapterMock) 51 | testAdapter.On("LoadPolicy").Return(nil, s.testError).Once() 52 | 53 | _, err := NewPolicyManager(testAdapter, false) 54 | 55 | assert.Error(s.T(), err) 56 | 57 | // Working adapter 58 | testAdapter = new(storageAdapterMock) 59 | testAdapter.On("LoadPolicy").Return(testPolicy, nil).Once() 60 | 61 | manager, err := NewPolicyManager(testAdapter, true) 62 | 63 | assert.Nil(s.T(), err) 64 | 65 | assert.IsType(s.T(), new(PolicyManager), manager) 66 | assert.Equal(s.T(), true, manager.autoUpdate) 67 | 68 | testAdapter.AssertNumberOfCalls(s.T(), "LoadPolicy", 1) 69 | } 70 | 71 | func (s *policyManagerSuite) TestLoadPolicy() { 72 | testPolicy := getBasicPolicy() 73 | 74 | // Working adapter 75 | testAdapter := new(storageAdapterMock) 76 | testAdapter.On("LoadPolicy").Return(testPolicy, nil).Once() 77 | 78 | manager, err := NewPolicyManager(testAdapter, false) 79 | 80 | assert.Nil(s.T(), err) 81 | 82 | testAdapter.On("LoadPolicy").Return(testPolicy, nil).Once() 83 | 84 | err = manager.LoadPolicy() 85 | 86 | assert.Nil(s.T(), err) 87 | assert.Equal(s.T(), testPolicy, manager.policy) 88 | // We expect 2, since NewPolicyManager calls LoadPolicy as well. 89 | testAdapter.AssertNumberOfCalls(s.T(), "LoadPolicy", 2) 90 | 91 | // Failing adapter 92 | testAdapter.On("LoadPolicy").Return(nil, s.testError).Once() 93 | 94 | err = manager.LoadPolicy() 95 | 96 | assert.Error(s.T(), err) 97 | } 98 | 99 | func (s *policyManagerSuite) TestLoadPolicy_ApplyPresets() { 100 | testPolicy := getBasicPolicy() 101 | 102 | testAdapter := new(storageAdapterMock) 103 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 104 | 105 | manager, _ := NewPolicyManager(testAdapter, false) 106 | 107 | testPolicy.PermissionPresets = PermissionPresets{ 108 | "testPreset1": &Permission{ 109 | Action: "test-action-1", 110 | }, 111 | "testPreset2": &Permission{ 112 | Action: "test-action-2", 113 | }, 114 | } 115 | 116 | testPermissions := Permissions{ 117 | &Permission{Preset: "testPreset1"}, 118 | &Permission{Preset: "testPreset2"}, 119 | } 120 | 121 | testPolicy.Roles[basicRoleOneName].Grants[basicResourceOneName] = testPermissions 122 | 123 | err := manager.LoadPolicy() 124 | 125 | assert.Nil(s.T(), err) 126 | assert.Equal(s.T(), "test-action-1", testPermissions[0].Action) 127 | assert.Equal(s.T(), "test-action-2", testPermissions[1].Action) 128 | } 129 | 130 | func (s *policyManagerSuite) TestLoadPolicy_ApplyPresetFailure() { 131 | testPolicy := getBasicPolicy() 132 | 133 | testAdapter := new(storageAdapterMock) 134 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 135 | 136 | manager, _ := NewPolicyManager(testAdapter, false) 137 | 138 | // applyPreset error handling - missing preset for Permission 139 | testPolicy.Roles[basicRoleOneName].Grants[basicResourceOneName][0].Preset = "incorrect-preset" 140 | 141 | err := manager.LoadPolicy() 142 | 143 | assert.Error(s.T(), err) 144 | assert.IsType(s.T(), new(PermissionPresetNotFoundError), err) 145 | } 146 | 147 | func (s *policyManagerSuite) TestSavePolicy() { 148 | testPolicy := getBasicPolicy() 149 | 150 | testAdapter := new(storageAdapterMock) 151 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 152 | 153 | manager, _ := NewPolicyManager(testAdapter, false) 154 | 155 | // Failing adapter 156 | testAdapter.On("SavePolicy", mock.Anything).Return(s.testError).Once() 157 | 158 | err := manager.SavePolicy() 159 | 160 | assert.Error(s.T(), err) 161 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 162 | testAdapter.AssertCalled(s.T(), "SavePolicy", testPolicy) 163 | 164 | // Working adapter 165 | testAdapter.On("SavePolicy", mock.Anything).Return(nil).Once() 166 | 167 | err = manager.SavePolicy() 168 | 169 | assert.Nil(s.T(), err) 170 | } 171 | 172 | func (s *policyManagerSuite) TestGetPolicy() { 173 | testPolicy := getBasicPolicy() 174 | 175 | testAdapter := new(storageAdapterMock) 176 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 177 | 178 | manager, _ := NewPolicyManager(testAdapter, false) 179 | 180 | policy := manager.GetPolicy() 181 | 182 | assert.Equal(s.T(), testPolicy, policy) 183 | } 184 | 185 | func (s *policyManagerSuite) TestGetRole() { 186 | testPolicy := getBasicPolicy() 187 | 188 | testAdapter := new(storageAdapterMock) 189 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 190 | 191 | manager, _ := NewPolicyManager(testAdapter, false) 192 | 193 | // Incorrect role 194 | role, err := manager.GetRole("INCORRECT_ROLE") 195 | 196 | assert.Nil(s.T(), role) 197 | assert.IsType(s.T(), new(RoleNotFoundError), err) 198 | 199 | // Correct role 200 | role, err = manager.GetRole(basicRoleOneName) 201 | 202 | assert.Equal(s.T(), testPolicy.Roles[basicRoleOneName], role) 203 | assert.Nil(s.T(), err) 204 | } 205 | 206 | func (s *policyManagerSuite) TestAddRole() { 207 | testPolicy := getBasicPolicy() 208 | 209 | testAdapter := new(storageAdapterMock) 210 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 211 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 212 | 213 | manager, _ := NewPolicyManager(testAdapter, false) 214 | 215 | // Existing role 216 | testExistingRole := &Role{ 217 | ID: basicRoleOneName, 218 | } 219 | 220 | err := manager.AddRole(testExistingRole) 221 | 222 | assert.IsType(s.T(), new(RoleAlreadyExistsError), err) 223 | 224 | // New role 225 | testNewRole := &Role{ID: "NEW_ROLE"} 226 | 227 | err = manager.AddRole(testNewRole) 228 | 229 | assert.Nil(s.T(), err) 230 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 231 | 232 | // With auto update 233 | testNewRole = &Role{ID: "NEW_ROLE_2"} 234 | 235 | manager.EnableAutoUpdate() 236 | 237 | _ = manager.AddRole(testNewRole) 238 | 239 | // It should still be one 240 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 241 | } 242 | 243 | func (s *policyManagerSuite) TestUpdateRole() { 244 | testPolicy := getBasicPolicy() 245 | 246 | testAdapter := new(storageAdapterMock) 247 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 248 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 249 | 250 | manager, _ := NewPolicyManager(testAdapter, false) 251 | 252 | // New role 253 | testNewRole := &Role{ 254 | ID: "NEW_ROLE", 255 | } 256 | 257 | err := manager.UpdateRole(testNewRole) 258 | 259 | assert.IsType(s.T(), new(RoleNotFoundError), err) 260 | 261 | // Existing role 262 | testExistingRole := &Role{ 263 | ID: basicRoleOneName, 264 | } 265 | 266 | err = manager.UpdateRole(testExistingRole) 267 | 268 | assert.Nil(s.T(), err) 269 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 270 | 271 | // With auto update 272 | manager.EnableAutoUpdate() 273 | 274 | _ = manager.UpdateRole(testExistingRole) 275 | 276 | // It should still be one 277 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 278 | } 279 | 280 | func (s *policyManagerSuite) TestUpsertRole() { 281 | testPolicy := getBasicPolicy() 282 | 283 | testAdapter := new(storageAdapterMock) 284 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 285 | 286 | manager, _ := NewPolicyManager(testAdapter, false) 287 | 288 | testNewRole := &Role{ 289 | ID: "NEW_ROLE", 290 | } 291 | 292 | testExistingRole := &Role{ 293 | ID: basicRoleOneName, 294 | } 295 | 296 | err := manager.UpsertRole(testNewRole) 297 | newRole, _ := manager.GetRole(testNewRole.ID) 298 | 299 | assert.Nil(s.T(), err) 300 | assert.Equal(s.T(), testNewRole, newRole) 301 | 302 | err = manager.UpsertRole(testExistingRole) 303 | existingRole, _ := manager.GetRole(testExistingRole.ID) 304 | 305 | assert.Nil(s.T(), err) 306 | assert.Equal(s.T(), testExistingRole, existingRole) 307 | } 308 | 309 | func (s *policyManagerSuite) TestDeleteRole() { 310 | testPolicy := getBasicPolicy() 311 | 312 | testAdapter := new(storageAdapterMock) 313 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 314 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 315 | 316 | manager, _ := NewPolicyManager(testAdapter, false) 317 | 318 | // Incorrect role, without auto update 319 | err := manager.DeleteRole("INCORRECT_ROLE") 320 | 321 | assert.IsType(s.T(), new(RoleNotFoundError), err) 322 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 323 | 324 | // Correct role, auto update 325 | manager.EnableAutoUpdate() 326 | 327 | err = manager.DeleteRole(basicRoleOneName) 328 | 329 | assert.Nil(s.T(), err) 330 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 331 | 332 | _, err = manager.GetRole(basicRoleOneName) 333 | 334 | assert.IsType(s.T(), new(RoleNotFoundError), err) 335 | } 336 | 337 | func (s *policyManagerSuite) TestAddPermission() { 338 | testPolicy := getBasicPolicy() 339 | 340 | testAdapter := new(storageAdapterMock) 341 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 342 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 343 | 344 | manager, _ := NewPolicyManager(testAdapter, false) 345 | 346 | testPermission := &Permission{ 347 | Action: createAction, 348 | } 349 | 350 | assert.Nil(s.T(), testPolicy.Roles[basicRoleOneName].Grants[basicResourceTwoName]) 351 | 352 | // Incorrect role, without auto update 353 | err := manager.AddPermission("INCORRECT_ROLE", basicResourceTwoName, testPermission) 354 | 355 | assert.IsType(s.T(), new(RoleNotFoundError), err) 356 | 357 | err = manager.AddPermission(basicRoleOneName, basicResourceTwoName, testPermission) 358 | 359 | assert.Nil(s.T(), err) 360 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 361 | 362 | // With auto update 363 | manager.EnableAutoUpdate() 364 | 365 | _ = manager.AddPermission(basicRoleOneName, basicResourceTwoName, testPermission) 366 | 367 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 368 | 369 | // Try preset application 370 | testPermission.Preset = "incorrect-preset" 371 | 372 | err = manager.AddPermission(basicRoleOneName, basicResourceTwoName, testPermission) 373 | 374 | assert.IsType(s.T(), new(PermissionPresetNotFoundError), err) 375 | } 376 | 377 | func (s *policyManagerSuite) TestDeletePermission() { 378 | testPolicy := getBasicPolicy() 379 | 380 | testAdapter := new(storageAdapterMock) 381 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 382 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 383 | 384 | manager, _ := NewPolicyManager(testAdapter, false) 385 | 386 | assert.Equal(s.T(), len(testPolicy.Roles[basicRoleOneName].Grants[basicResourceOneName]), 2) 387 | 388 | // Incorrect role, without auto update 389 | err := manager.DeletePermission("INCORRECT_ROLE", basicResourceOneName, createAction) 390 | 391 | assert.IsType(s.T(), new(RoleNotFoundError), err) 392 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 393 | 394 | // Correct role, auto update 395 | manager.EnableAutoUpdate() 396 | 397 | err = manager.DeletePermission(basicRoleOneName, basicResourceOneName, createAction) 398 | 399 | assert.Nil(s.T(), err) 400 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 401 | 402 | role, _ := manager.GetRole(basicRoleOneName) 403 | 404 | assert.Equal(s.T(), len(role.Grants[basicResourceOneName]), 1) 405 | 406 | // Incorrect action (do nothing) 407 | err = manager.DeletePermission(basicRoleOneName, basicResourceOneName, "incorrect-action") 408 | 409 | assert.Nil(s.T(), err) 410 | } 411 | 412 | func (s *policyManagerSuite) TestAddPermissionPreset() { 413 | testPolicy := getBasicPolicy() 414 | 415 | testAdapter := new(storageAdapterMock) 416 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 417 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 418 | 419 | manager, _ := NewPolicyManager(testAdapter, false) 420 | 421 | assert.Nil(s.T(), testPolicy.PermissionPresets) 422 | 423 | testPresetName := "TestPreset" 424 | 425 | testPreset := &Permission{ 426 | Action: "test-action-2", 427 | } 428 | 429 | // Preset exists 430 | testPolicy.PermissionPresets = PermissionPresets{ 431 | testPresetName: testPreset, 432 | } 433 | 434 | err := manager.AddPermissionPreset(testPresetName, testPreset) 435 | 436 | assert.IsType(s.T(), new(PermissionPresetAlreadyExistsError), err) 437 | 438 | // Presets set to nil, Preset does not exist, without auto update 439 | testPolicy.PermissionPresets = nil 440 | 441 | err = manager.AddPermissionPreset(testPresetName, testPreset) 442 | 443 | policy := manager.GetPolicy() 444 | 445 | assert.Nil(s.T(), err) 446 | assert.Equal(s.T(), testPreset, policy.PermissionPresets[testPresetName]) 447 | 448 | // With auto update 449 | manager.EnableAutoUpdate() 450 | 451 | //nolint 452 | manager.AddPermissionPreset("SecondTestPreset", testPreset) 453 | 454 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 455 | } 456 | 457 | func (s *policyManagerSuite) TestUpdatePermissionPreset() { 458 | testPolicy := getBasicPolicy() 459 | 460 | testAdapter := new(storageAdapterMock) 461 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 462 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 463 | 464 | manager, _ := NewPolicyManager(testAdapter, false) 465 | 466 | testPresetName := "TestPreset" 467 | testPreset := &Permission{ 468 | Action: "test-action-2", 469 | } 470 | 471 | // Preset does not exist 472 | testPolicy.PermissionPresets = PermissionPresets{ 473 | testPresetName: testPreset, 474 | } 475 | 476 | testIncorrectPreset := &Permission{ 477 | Action: "test-action-2", 478 | } 479 | 480 | err := manager.UpdatePermissionPreset("IncorrectName", testIncorrectPreset) 481 | 482 | assert.IsType(s.T(), new(PermissionPresetNotFoundError), err) 483 | 484 | // Preset exists 485 | err = manager.UpdatePermissionPreset(testPresetName, testPreset) 486 | 487 | preset := manager.getPermissionPreset(testPresetName) 488 | 489 | assert.Nil(s.T(), err) 490 | assert.Equal(s.T(), testPreset.Action, preset.Action) 491 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 492 | 493 | // With auto update 494 | manager.EnableAutoUpdate() 495 | //nolint 496 | manager.UpdatePermissionPreset(testPresetName, testPreset) 497 | 498 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 499 | } 500 | 501 | func (s *policyManagerSuite) TestUpsertPermissionPreset() { 502 | testPolicy := getBasicPolicy() 503 | 504 | testAdapter := new(storageAdapterMock) 505 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 506 | 507 | manager, _ := NewPolicyManager(testAdapter, false) 508 | 509 | testPresetName := "TestPreset" 510 | testPreset := &Permission{ 511 | Action: "test-action-2", 512 | } 513 | 514 | // Preset does not exist 515 | err := manager.UpsertPermissionPreset(testPresetName, testPreset) 516 | preset := manager.getPermissionPreset(testPresetName) 517 | 518 | assert.Nil(s.T(), err) 519 | assert.Equal(s.T(), testPreset, preset) 520 | 521 | err = manager.UpsertPermissionPreset(testPresetName, testPreset) 522 | preset = manager.getPermissionPreset(testPresetName) 523 | 524 | assert.Nil(s.T(), err) 525 | assert.Equal(s.T(), testPreset, preset) 526 | } 527 | 528 | func (s *policyManagerSuite) TestDeletePermissionPreset() { 529 | testPolicy := getBasicPolicy() 530 | 531 | testAdapter := new(storageAdapterMock) 532 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 533 | testAdapter.On("SavePolicy", mock.Anything).Return(nil) 534 | 535 | testPresetName := "TestPreset" 536 | testPreset := &Permission{ 537 | Action: "test-action-2", 538 | } 539 | 540 | testPolicy.PermissionPresets = PermissionPresets{ 541 | testPresetName: testPreset, 542 | "TestPreset2": &Permission{ 543 | Action: "test-action-2", 544 | }, 545 | "TestPreset3": &Permission{ 546 | Action: "test-action-2", 547 | }, 548 | } 549 | 550 | manager, _ := NewPolicyManager(testAdapter, false) 551 | 552 | assert.Equal(s.T(), len(testPolicy.PermissionPresets), 3) 553 | 554 | // Incorrect preset, without auto update 555 | err := manager.DeletePermissionPreset("INCORRECT_PRESET") 556 | 557 | assert.IsType(s.T(), new(PermissionPresetNotFoundError), err) 558 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 0) 559 | 560 | // Correct role, auto update 561 | manager.EnableAutoUpdate() 562 | 563 | err = manager.DeletePermissionPreset(testPresetName) 564 | 565 | assert.Nil(s.T(), err) 566 | testAdapter.AssertNumberOfCalls(s.T(), "SavePolicy", 1) 567 | 568 | policy := manager.GetPolicy() 569 | 570 | assert.Equal(s.T(), len(policy.PermissionPresets), 2) 571 | } 572 | 573 | func (s *policyManagerSuite) TestDisableAutoUpdate() { 574 | testPolicy := getBasicPolicy() 575 | 576 | testAdapter := new(storageAdapterMock) 577 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 578 | 579 | manager, _ := NewPolicyManager(testAdapter, true) 580 | 581 | assert.True(s.T(), manager.autoUpdate) 582 | 583 | manager.DisableAutoUpdate() 584 | 585 | assert.False(s.T(), manager.autoUpdate) 586 | 587 | } 588 | 589 | func (s *policyManagerSuite) TestEnableAutoUpdate() { 590 | testPolicy := getBasicPolicy() 591 | 592 | testAdapter := new(storageAdapterMock) 593 | testAdapter.On("LoadPolicy").Return(testPolicy, nil) 594 | 595 | manager, _ := NewPolicyManager(testAdapter, false) 596 | 597 | assert.False(s.T(), manager.autoUpdate) 598 | 599 | manager.EnableAutoUpdate() 600 | 601 | assert.True(s.T(), manager.autoUpdate) 602 | 603 | } 604 | -------------------------------------------------------------------------------- /access_manager_test.go: -------------------------------------------------------------------------------- 1 | package restrict 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type policyProviderMock struct { 13 | mock.Mock 14 | } 15 | 16 | func (m *policyProviderMock) GetRole(name string) (*Role, error) { 17 | args := m.Called(name) 18 | 19 | if args.Get(0) == nil { 20 | return nil, args.Error(1) 21 | } 22 | 23 | return args.Get(0).(*Role), args.Error(1) 24 | } 25 | 26 | type accessManagerSuite struct { 27 | suite.Suite 28 | 29 | testError error 30 | } 31 | 32 | func (s *accessManagerSuite) SetupSuite() { 33 | s.testError = errors.New("testError") 34 | } 35 | 36 | func TestAccessManagerSuite(t *testing.T) { 37 | suite.Run(t, new(accessManagerSuite)) 38 | } 39 | 40 | func (s *accessManagerSuite) TestNewAccessManager() { 41 | testPolicyProvider := new(policyProviderMock) 42 | 43 | manager := NewAccessManager(testPolicyProvider) 44 | 45 | assert.NotNil(s.T(), manager) 46 | assert.IsType(s.T(), new(AccessManager), manager) 47 | } 48 | 49 | func (s *accessManagerSuite) TestAuthorize_MalformedRequest() { 50 | testPolicyProvider := new(policyProviderMock) 51 | testPolicyProvider.On("GetRole", mock.Anything).Return(nil, nil) 52 | 53 | manager := NewAccessManager(testPolicyProvider) 54 | 55 | testResource := new(resourceMock) 56 | 57 | testRequest := &AccessRequest{ 58 | Subject: nil, 59 | Resource: testResource, 60 | } 61 | 62 | err := manager.Authorize(testRequest) 63 | 64 | assert.IsType(s.T(), new(RequestMalformedError), err) 65 | 66 | testSubject := new(subjectMock) 67 | 68 | testRequest.Subject = testSubject 69 | testRequest.Resource = nil 70 | 71 | err = manager.Authorize(testRequest) 72 | 73 | assert.IsType(s.T(), new(RequestMalformedError), err) 74 | } 75 | 76 | func (s *accessManagerSuite) TestAuthorize_MalformedSubjectOrResource() { 77 | testPolicyProvider := new(policyProviderMock) 78 | testPolicyProvider.On("GetRole", mock.Anything).Return(getBasicRoleOne(), nil) 79 | 80 | manager := NewAccessManager(testPolicyProvider) 81 | 82 | testSubject := new(subjectMock) 83 | testResource := new(resourceMock) 84 | 85 | // Failing Subject, working Resource. 86 | testSubject.On("GetRoles").Return([]string{}).Once() 87 | testResource.On("GetResourceName").Return(basicResourceOneName).Once() 88 | 89 | testRequest := &AccessRequest{ 90 | Subject: testSubject, 91 | Resource: testResource, 92 | } 93 | 94 | err := manager.Authorize(testRequest) 95 | 96 | assert.IsType(s.T(), new(RequestMalformedError), err) 97 | 98 | testSubject.AssertNumberOfCalls(s.T(), "GetRoles", 1) 99 | testResource.AssertNumberOfCalls(s.T(), "GetResourceName", 1) 100 | 101 | // Working Subject, failing Resource. 102 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 103 | testResource.On("GetResourceName").Return("").Once() 104 | 105 | err = manager.Authorize(testRequest) 106 | 107 | assert.IsType(s.T(), new(RequestMalformedError), err) 108 | 109 | testResource.On("GetResourceName").Return(basicResourceOneName).Once() 110 | 111 | err = manager.Authorize(testRequest) 112 | 113 | // Note that err is nil because we actually supplied providerMock with correct Role. 114 | // Otherwise, err would still be set, but with different type. 115 | assert.Nil(s.T(), err) 116 | } 117 | 118 | func (s *accessManagerSuite) TestAuthorize_NoPermissions() { 119 | // Failing GetRole check. 120 | testPolicyProvider := new(policyProviderMock) 121 | testPolicyProvider.On("GetRole", mock.Anything).Return(nil, s.testError).Once() 122 | 123 | manager := NewAccessManager(testPolicyProvider) 124 | 125 | testSubject := new(subjectMock) 126 | testResource := new(resourceMock) 127 | 128 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 129 | testResource.On("GetResourceName").Return(basicResourceOneName) 130 | 131 | testRequest := &AccessRequest{ 132 | Subject: testSubject, 133 | Resource: testResource, 134 | Actions: []string{"testAction"}, 135 | } 136 | 137 | err := manager.Authorize(testRequest) 138 | 139 | testPolicyProvider.AssertNumberOfCalls(s.T(), "GetRole", 1) 140 | assert.Error(s.T(), err) 141 | 142 | testRole := getBasicRoleOne() 143 | 144 | // Empty grants check. 145 | testRole.Grants = nil 146 | testPolicyProvider.On("GetRole", mock.Anything).Return(testRole, nil) 147 | 148 | err = manager.Authorize(testRequest) 149 | 150 | assert.IsType(s.T(), new(AccessDeniedError), err) 151 | 152 | // 0 length grants. 153 | testRole.Grants = GrantsMap{} 154 | 155 | err = manager.Authorize(testRequest) 156 | 157 | assert.IsType(s.T(), new(AccessDeniedError), err) 158 | } 159 | 160 | func (s *accessManagerSuite) TestAuthorize_ActionsWithoutConditions() { 161 | testRole := getBasicRoleOne() 162 | 163 | testPolicyProvider := new(policyProviderMock) 164 | testPolicyProvider.On("GetRole", mock.Anything).Return(testRole, nil) 165 | 166 | manager := NewAccessManager(testPolicyProvider) 167 | 168 | testSubject := new(subjectMock) 169 | testResource := new(resourceMock) 170 | 171 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 172 | testResource.On("GetResourceName").Return(basicResourceOneName) 173 | 174 | // Action does not exist on role. 175 | testRequest := &AccessRequest{ 176 | Subject: testSubject, 177 | Resource: testResource, 178 | Actions: []string{deleteAction}, 179 | } 180 | 181 | err := manager.Authorize(testRequest) 182 | 183 | assert.IsType(s.T(), new(AccessDeniedError), err) 184 | assert.IsType(s.T(), new(PermissionError), err.(*AccessDeniedError).FirstReason()) 185 | 186 | // One of the actions does not exist on role. 187 | testRequest.Actions = []string{createAction, deleteAction} 188 | err = manager.Authorize(testRequest) 189 | 190 | assert.IsType(s.T(), new(AccessDeniedError), err) 191 | assert.IsType(s.T(), new(PermissionError), err.(*AccessDeniedError).FirstReason()) 192 | 193 | // One of the actions is empty string. 194 | testRequest.Actions = []string{createAction, ""} 195 | err = manager.Authorize(testRequest) 196 | 197 | assert.IsType(s.T(), new(RequestMalformedError), err) 198 | 199 | // Action exists on role. 200 | testRequest.Actions = []string{createAction} 201 | err = manager.Authorize(testRequest) 202 | 203 | assert.Nil(s.T(), err) 204 | } 205 | 206 | func (s *accessManagerSuite) TestAuthorize_ActionsWithConditions() { 207 | testConditionedAction := "conditioned-action" 208 | 209 | // Working Condition. 210 | testWorkingCondition := new(conditionMock) 211 | testWorkingCondition.On("Check", mock.Anything).Return(nil) 212 | 213 | testConditionedPermission := &Permission{ 214 | Action: testConditionedAction, 215 | Conditions: Conditions{testWorkingCondition}, 216 | } 217 | testRole := getBasicRoleOne() 218 | testRole.Grants[basicResourceOneName] = append(testRole.Grants[basicResourceOneName], testConditionedPermission) 219 | 220 | testPolicyProvider := new(policyProviderMock) 221 | testPolicyProvider.On("GetRole", mock.Anything).Return(testRole, nil) 222 | 223 | manager := NewAccessManager(testPolicyProvider) 224 | 225 | testSubject := new(subjectMock) 226 | testResource := new(resourceMock) 227 | 228 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 229 | testResource.On("GetResourceName").Return(basicResourceOneName) 230 | 231 | testRequest := &AccessRequest{ 232 | Subject: testSubject, 233 | Resource: testResource, 234 | Actions: []string{testConditionedAction}, 235 | } 236 | 237 | err := manager.Authorize(testRequest) 238 | 239 | assert.Nil(s.T(), err) 240 | 241 | // Failing Condition 242 | testFailingCondition := new(conditionMock) 243 | testConditionError := NewConditionNotSatisfiedError(testFailingCondition, testRequest, s.testError) 244 | 245 | testFailingCondition.On("Check", mock.Anything).Return(testConditionError) 246 | 247 | testConditionedPermission.Conditions = Conditions{testFailingCondition} 248 | 249 | err = manager.Authorize(testRequest) 250 | permissionErr := err.(*AccessDeniedError).FirstReason() 251 | conditionError := permissionErr.FirstConditionError() 252 | 253 | assert.IsType(s.T(), new(AccessDeniedError), err) 254 | assert.IsType(s.T(), new(PermissionError), permissionErr) 255 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), conditionError) 256 | 257 | // AND - should expect all Conditions to be satisfied 258 | testConditionedPermission.Conditions = Conditions{testWorkingCondition, testWorkingCondition, testFailingCondition} 259 | 260 | err = manager.Authorize(testRequest) 261 | permissionErr = err.(*AccessDeniedError).FirstReason() 262 | conditionError = permissionErr.FirstConditionError() 263 | 264 | assert.IsType(s.T(), new(AccessDeniedError), err) 265 | assert.IsType(s.T(), new(PermissionError), permissionErr) 266 | assert.IsType(s.T(), new(ConditionNotSatisfiedError), conditionError) 267 | 268 | // OR - should expect one of Permissions to be granted 269 | testConditionedPermission.Conditions = Conditions{testWorkingCondition, testFailingCondition} 270 | 271 | secondTestConditionedPermission := &Permission{ 272 | Action: testConditionedAction, 273 | Conditions: Conditions{testWorkingCondition}, 274 | } 275 | 276 | testRole.Grants[basicResourceOneName] = append(testRole.Grants[basicResourceOneName], secondTestConditionedPermission) 277 | 278 | err = manager.Authorize(testRequest) 279 | 280 | assert.Nil(s.T(), err) 281 | } 282 | 283 | func (s *accessManagerSuite) TestAuthorize_UnknownConditionError() { 284 | testConditionedAction := "conditioned-action" 285 | testRole := getBasicRoleOne() 286 | 287 | testParentRole := getBasicParentRole() 288 | 289 | testRole.Parents = []string{testParentRole.ID} 290 | 291 | testPolicyProvider := new(policyProviderMock) 292 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRole, nil) 293 | testPolicyProvider.On("GetRole", basicParentRoleName).Return(testParentRole, nil) 294 | 295 | // Failing Condition 296 | testFailingCondition := new(conditionMock) 297 | testConditionError := errors.New("Custom error") 298 | 299 | testFailingCondition.On("Check", mock.Anything).Return(testConditionError) 300 | 301 | testConditionedPermission := &Permission{ 302 | Action: testConditionedAction, 303 | Conditions: Conditions{testFailingCondition}, 304 | } 305 | 306 | testRole.Grants[basicResourceOneName] = append(testRole.Grants[basicResourceOneName], testConditionedPermission) 307 | 308 | manager := NewAccessManager(testPolicyProvider) 309 | 310 | testSubject := new(subjectMock) 311 | testResource := new(resourceMock) 312 | 313 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 314 | testResource.On("GetResourceName").Return(basicResourceOneName) 315 | 316 | testRequest := &AccessRequest{ 317 | Subject: testSubject, 318 | Resource: testResource, 319 | Actions: []string{testConditionedAction}, 320 | } 321 | 322 | err := manager.Authorize(testRequest) 323 | 324 | assert.Equal(s.T(), testConditionError, err) 325 | 326 | // Unknown error on parents 327 | testRole.Grants[basicResourceOneName] = Permissions{} 328 | testParentRole.Grants[basicResourceOneName] = append(testParentRole.Grants[basicResourceOneName], testConditionedPermission) 329 | 330 | err = manager.Authorize(testRequest) 331 | 332 | assert.Equal(s.T(), testConditionError, err) 333 | } 334 | 335 | func (s *accessManagerSuite) TestAuthorize_ActionsOnParents() { 336 | testRole := getBasicRoleOne() 337 | testParentRole := getBasicParentRole() 338 | 339 | testRole.Parents = []string{testParentRole.ID} 340 | 341 | testPolicyProvider := new(policyProviderMock) 342 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRole, nil) 343 | testPolicyProvider.On("GetRole", basicParentRoleName).Return(testParentRole, nil) 344 | 345 | manager := NewAccessManager(testPolicyProvider) 346 | 347 | testSubject := new(subjectMock) 348 | testResource := new(resourceMock) 349 | 350 | testSubject.On("GetRoles").Return(getBasicRolesSet()) 351 | testResource.On("GetResourceName").Return(basicResourceOneName) 352 | 353 | // Action exist on parent. 354 | testRequest := &AccessRequest{ 355 | Subject: testSubject, 356 | Resource: testResource, 357 | Actions: []string{updateAction}, 358 | } 359 | 360 | err := manager.Authorize(testRequest) 361 | 362 | assert.Nil(s.T(), err) 363 | 364 | // Action does not exist on parent. 365 | testRequest.Actions = []string{deleteAction} 366 | 367 | err = manager.Authorize(testRequest) 368 | 369 | assert.IsType(s.T(), new(AccessDeniedError), err) 370 | assert.IsDecreasing(s.T(), new(PermissionError), err.(*AccessDeniedError).FirstReason()) 371 | 372 | testGrantParentRoleName := "BasicGrandParent" 373 | testGrandParentRole := getBasicParentRole() 374 | 375 | testGrandParentRole.ID = testGrantParentRoleName 376 | testGrandParentRole.Grants[basicResourceOneName] = 377 | append(testGrandParentRole.Grants[basicResourceOneName], &Permission{Action: deleteAction}) 378 | 379 | testPolicyProvider.On("GetRole", testGrantParentRoleName).Return(testGrandParentRole, nil) 380 | 381 | testParentRole.Parents = []string{testGrantParentRoleName} 382 | 383 | // Action exist on grandparent. 384 | testRequest.Actions = []string{deleteAction} 385 | 386 | err = manager.Authorize(testRequest) 387 | 388 | assert.Nil(s.T(), err) 389 | 390 | // Ignore inheritance cycle when permission is granted beforehand. 391 | testGrandParentRole.Parents = []string{testRole.ID} 392 | 393 | err = manager.Authorize(testRequest) 394 | 395 | assert.Nil(s.T(), err) 396 | 397 | // Detect inheritance cycle when permission is not granted beforehand. 398 | testRequest.Actions = []string{"NewAction"} 399 | 400 | err = manager.Authorize(testRequest) 401 | 402 | assert.IsType(s.T(), new(RoleInheritanceCycleError), err) 403 | } 404 | 405 | func (s *accessManagerSuite) TestAuthorize_MultipleRoles() { 406 | testRoleOne := getBasicRoleOne() 407 | testRoleTwo := getBasicRoleTwo() 408 | 409 | testMissingAction := "missing-action" 410 | 411 | testPolicyProvider := new(policyProviderMock) 412 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRoleOne, nil) 413 | testPolicyProvider.On("GetRole", basicRoleTwoName).Return(testRoleTwo, nil) 414 | 415 | manager := NewAccessManager(testPolicyProvider) 416 | 417 | testSubject := new(subjectMock) 418 | testResource := new(resourceMock) 419 | 420 | testSubject.On("GetRoles").Return([]string{basicRoleOneName, basicRoleTwoName}) 421 | testResource.On("GetResourceName").Return(basicResourceOneName) 422 | 423 | // Action does not exist on neither role. 424 | testRequest := &AccessRequest{ 425 | Subject: testSubject, 426 | Resource: testResource, 427 | Actions: []string{testMissingAction, "delete"}, 428 | } 429 | 430 | err := manager.Authorize(testRequest) 431 | accessError := err.(*AccessDeniedError) 432 | 433 | assert.IsType(s.T(), new(AccessDeniedError), err) 434 | assert.True(s.T(), len(accessError.Reasons) == 2) 435 | 436 | roleOneErrors := accessError.Reasons.GetByRoleName(basicRoleOneName) 437 | assert.True(s.T(), len(roleOneErrors) == 1) 438 | assert.True(s.T(), roleOneErrors[0].Action == testMissingAction) 439 | 440 | roleTwoErrors := accessError.Reasons.GetByRoleName(basicRoleTwoName) 441 | assert.True(s.T(), len(roleTwoErrors) == 1) 442 | assert.True(s.T(), roleTwoErrors[0].Action == testMissingAction) 443 | 444 | assert.True(s.T(), len(accessError.Reasons.GetByAction(testMissingAction)) == 2) 445 | 446 | // Action exists on one of the roles. 447 | testRequest.Actions = []string{deleteAction} 448 | err = manager.Authorize(testRequest) 449 | 450 | assert.Nil(s.T(), err) 451 | } 452 | 453 | func (s *accessManagerSuite) TestAuthorize_FailEarlyValidation() { 454 | testRoleOne := getBasicRoleOne() 455 | 456 | missingActions := []string{"missing-action-one", "missing-action-two", "missing-action-three"} 457 | actions := append([]string{readAction}, missingActions...) 458 | 459 | testPolicyProvider := new(policyProviderMock) 460 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRoleOne, nil) 461 | 462 | manager := NewAccessManager(testPolicyProvider) 463 | 464 | testSubject := new(subjectMock) 465 | testResource := new(resourceMock) 466 | 467 | testSubject.On("GetRoles").Return([]string{basicRoleOneName}) 468 | testResource.On("GetResourceName").Return(basicResourceOneName) 469 | 470 | // Missing Permission for Actions 471 | testRequest := &AccessRequest{ 472 | Subject: testSubject, 473 | Resource: testResource, 474 | Actions: actions, 475 | } 476 | 477 | err := manager.Authorize(testRequest) 478 | accessError := err.(*AccessDeniedError) 479 | 480 | // Expected 1 PermissionError despite 3 potential Permission Errors. 481 | // The first returned error should be the one for the first Action declared in the AccessRequest. 482 | assert.True(s.T(), len(accessError.Reasons) == 1) 483 | assert.True(s.T(), accessError.FirstReason().Action == missingActions[0]) 484 | } 485 | 486 | func (s *accessManagerSuite) TestAuthorize_CompleteValidationSingleRole() { 487 | testRole := getBasicRoleOne() 488 | 489 | missingActions := []string{"missing-action-one", "missing-action-two", "missing-action-three"} 490 | actions := append([]string{readAction}, missingActions...) 491 | 492 | testPolicyProvider := new(policyProviderMock) 493 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRole, nil) 494 | 495 | manager := NewAccessManager(testPolicyProvider) 496 | 497 | testSubject := new(subjectMock) 498 | testResource := new(resourceMock) 499 | 500 | testSubject.On("GetRoles").Return([]string{basicRoleOneName}) 501 | testResource.On("GetResourceName").Return(basicResourceOneName) 502 | 503 | // Missing Permission for Actions 504 | testRequest := &AccessRequest{ 505 | Subject: testSubject, 506 | Resource: testResource, 507 | Actions: actions, 508 | CompleteValidation: true, 509 | } 510 | 511 | err := manager.Authorize(testRequest) 512 | accessError := err.(*AccessDeniedError) 513 | 514 | // Expected 3 PermissionErrors for 3 missing Actions. 515 | // The order should match the order of Actions passed in the AccessRequest, 516 | // and should be preserved in the returned error. 517 | assert.True(s.T(), len(accessError.Reasons) == 3) 518 | 519 | for i, permissionErr := range accessError.Reasons { 520 | assert.True(s.T(), permissionErr.Action == missingActions[i]) 521 | } 522 | 523 | // Missing Permission for Actions and some failed Conditions 524 | failingCondition := new(conditionMock) 525 | conditionError := NewConditionNotSatisfiedError(failingCondition, testRequest, s.testError) 526 | 527 | failingCondition.On("Check", mock.Anything).Return(conditionError) 528 | 529 | conditionedPermission := &Permission{ 530 | Action: deleteAction, 531 | Conditions: Conditions{failingCondition, failingCondition}, 532 | } 533 | 534 | testRole.Grants[basicResourceOneName] = append(testRole.Grants[basicResourceOneName], conditionedPermission) 535 | 536 | testRequest.Actions = append([]string{deleteAction}, testRequest.Actions...) 537 | 538 | err = manager.Authorize(testRequest) 539 | accessError = err.(*AccessDeniedError) 540 | 541 | // Expected 4 PermissionErrors - one for read Action with failing Conditions, 542 | // and 3 for missing Actions. 543 | // The order should again match the order of Actions passed in the Request. 544 | assert.True(s.T(), len(accessError.Reasons) == 4) 545 | 546 | assert.True(s.T(), accessError.Reasons[0].Action == deleteAction) 547 | assert.True(s.T(), accessError.Reasons[1].Action == missingActions[0]) 548 | assert.True(s.T(), accessError.Reasons[2].Action == missingActions[1]) 549 | assert.True(s.T(), accessError.Reasons[3].Action == missingActions[2]) 550 | 551 | permissionErrWithConditions := accessError.Reasons[0] 552 | 553 | // Expecting 2 ConditionErrors, each for one failingConditions in the Grants. 554 | assert.True(s.T(), len(permissionErrWithConditions.ConditionErrors) == 2) 555 | 556 | for _, conditionErr := range permissionErrWithConditions.ConditionErrors { 557 | assert.True(s.T(), conditionErr.Reason.Error() == s.testError.Error()) 558 | assert.True(s.T(), conditionErr.Condition == failingCondition) 559 | } 560 | } 561 | 562 | func (s *accessManagerSuite) TestAuthorize_CompleteValidationMultipleRoles() { 563 | testRoleOne := getBasicRoleOne() 564 | testRoleTwo := getBasicRoleTwo() 565 | 566 | missingActions := []string{"missing-action-one", "missing-action-two", "missing-action-three"} 567 | actions := append([]string{readAction}, missingActions...) 568 | 569 | testPolicyProvider := new(policyProviderMock) 570 | testPolicyProvider.On("GetRole", basicRoleOneName).Return(testRoleOne, nil) 571 | testPolicyProvider.On("GetRole", basicRoleTwoName).Return(testRoleTwo, nil) 572 | 573 | manager := NewAccessManager(testPolicyProvider) 574 | 575 | testSubject := new(subjectMock) 576 | testResource := new(resourceMock) 577 | 578 | testSubject.On("GetRoles").Return([]string{basicRoleOneName, basicRoleTwoName}) 579 | testResource.On("GetResourceName").Return(basicResourceOneName) 580 | 581 | // Missing Permission for Actions 582 | testRequest := &AccessRequest{ 583 | Subject: testSubject, 584 | Resource: testResource, 585 | Actions: actions, 586 | CompleteValidation: true, 587 | } 588 | 589 | err := manager.Authorize(testRequest) 590 | accessError := err.(*AccessDeniedError) 591 | 592 | // Expected 6 PermissionErrors for 3 missing Actions on each Role. 593 | // The order should be determined first by the Roles order in the Subject's GetRoles() result, 594 | // and then by Actions passed in AccessRequest. 595 | assert.True(s.T(), len(accessError.Reasons) == 6) 596 | 597 | for i, permissionErr := range accessError.Reasons[0:3] { 598 | assert.True(s.T(), permissionErr.Action == missingActions[i]) 599 | assert.True(s.T(), permissionErr.RoleName == basicRoleOneName) 600 | } 601 | 602 | for i, permissionErr := range accessError.Reasons[3:6] { 603 | assert.True(s.T(), permissionErr.Action == missingActions[i]) 604 | assert.True(s.T(), permissionErr.RoleName == basicRoleTwoName) 605 | } 606 | 607 | roleOneErrors := accessError.Reasons.GetByRoleName(basicRoleOneName) 608 | roleTwoErrors := accessError.Reasons.GetByRoleName(basicRoleTwoName) 609 | 610 | assert.True(s.T(), len(roleOneErrors) == 3) 611 | assert.True(s.T(), roleOneErrors[0].RoleName == basicRoleOneName) 612 | 613 | assert.True(s.T(), len(roleTwoErrors) == 3) 614 | assert.True(s.T(), roleTwoErrors[0].RoleName == basicRoleTwoName) 615 | } 616 | --------------------------------------------------------------------------------