restrict

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 14, 2021 License: MIT Imports: 7 Imported by: 5

README

restrict

Go Report Card License Tests Go Reference Release

Restrict is an authorization library that provides a hybrid of RBAC and ABAC models, allowing to define simple role-based policies while using more fine-grained control when needed. It helps you keep enforcing your access policies away from the business logic, and express them in a convenient way.

Table of contents

Installation

To install the library, run:

go get github.com/el-mike/restrict

Go version 1.15+ is required!
Restrict follows semantic versioning, so any changes will be applied according to its principles.

Concepts

Restrict helps with building simple yet powerful access policies in declarative way. In order to do that, we introduce following concepts:

  • Subject - an entity that wants to perform some actions. Needs to implement Subject interface and provide unique role name. Subject is usually any kind of user or client in your domain.
  • Resource - an entity that is a target of the actions. Needs to implement Resource interface and provide unique resource name. Resource can be implemented by any entity or object in your domain.
  • Action - an arbitrary operation that can be performed on given Resource.
  • Context - a map of values containing any additional data needed to validate the access rights.
  • Condition - requirement that needs to be satisfied in order to grant the access. There are couple of built-in Conditions, but any custom Condition can be added, as long as it implements Condition interface. Conditions are the way to express more granular control.

Restrict uses those informations to determine whether an access can be granted.

Basic usage

type User struct {
	ID string
}

// Subject interface implementation.
func (u *User) GetRole() string {
	return "User"
}

// Example entity with some fields.
type Conversation struct {
	ID           string
	CreatedBy    string
	Participants []string
	Active       bool
}

// Resource interface implementation.
func (c *Conversation) GetResourceName() string {
	return "Conversation"
}


var policy = &restrict.PolicyDefinition{
	Roles: restrict.Roles{
		"User": {
			Grants: restrict.GrantsMap{
				"Conversation": {
					&restrict.Permission{Action: "read"},
					&restrict.Permission{Action: "create"},
				},
			},
		},
	},
}

func main() {
	// Create an instance of PolicyManager, which will be responsible for handling given PolicyDefinition.
	// You can use one of the built-in persistence adapters (in-memory or json/yaml file adapters), or provide your own.
	policyMananger, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(policy), true)
	if err != nil {
		log.Fatal(err)
	}

	manager := restrict.NewAccessManager(policyMananger)

	if err = manager.Authorize(&restrict.AccessRequest{
		Subject:  &User{},
		Resource: &Conversation{},
		Actions:  []string{"read", "delete"},
	}); err != nil {
		fmt.Print(err) // Access denied for action: "delete". Reason: Permission for action: "delete" is not granted for Resource: "Conversation"

	}
}

Policy

Policy is the description of access rules that should be enforced in given system. It consists of a Roles map, each with a set of Permissions granted per Resource, and Permission presets, that can be reused under various Roles and Resources. Here is an example of a policy:

var policy = &restrict.PolicyDefinition{
	// A map of Roles. Key corresponds to a Role that Subjects in your system can belong to.
	Roles: restrict.Roles{
		"User": {
			// Optional, human readable description.
			Description: "This is a simple User role, with permissions for basic chat operations.",
			// Map of Permissions per Resource.
			// Grants map can be nil, meaning given Role has no Permissions (but can still inherit some).
			Grants: restrict.GrantsMap{
				"Conversation": {
					// Subject "User" can "read" any "Conversation".
					&restrict.Permission{Action: "read"},
					// Subject "User" can "create" a "Conversation".
					&restrict.Permission{Action: "create"},
					// Subject "User" can "update" ONLY a "Coversation" that was
					// created by it. Check "updateOwn" preset definition below.
					&restrict.Permission{Preset: "updateOwn"},
					// Subject "User" can "delete" ONLY inactive "Conversation".
					&restrict.Permission{
						Action: "delete",
						Conditions: restrict.Conditions{
							// EmptyCondition requires a value (described by ValueDescriptor)
							// to be empty (falsy) in order to grant the access.
							// In this example, we want Conversation.Active to be false.
							&restrict.EmptyCondition{
								ID: "deleteActive",
								Value: &restrict.ValueDescriptor{
									Source: restrict.ResourceField,
									Field:  "Active",
								},
							},
						},
					},
				},
			},
		},
		"Admin": {
			Description: "This is an Admin role, with permissions to manage Users.",
			// "Admin" can do everything "User" can.
			Parents: []string{"User"},
			// AND can also perform other operations that User itself
			// is not allowed to do.
			Grants: restrict.GrantsMap{
				// Please note that in order to make this work,
				// User needs to implement Resource interface.
				"User": {
					// Subject "Admin" can create a "User".
					&restrict.Permission{Action: "create"},
				},
			},
		},
	},
	// A map of reusable Permissions. Key corresponds is a preset's name.
	PermissionPresets: restrict.PermissionPresets{
		"updateOwn": &restrict.Permission{
			// An action that given Permission allows to perform.
			Action: "update",
			// Optional Conditions that when defined, need to be satisfied in order
			// to allow the access.
			Conditions: restrict.Conditions{
				// EqualCondition requires two values (described by ValueDescriptors)
				// to be equal in order to grant the access.
				// In this example we want to check if Conversation.CreatedBy and User.ID
				// are the same, meaning that Conversation was created by given User.
				&restrict.EqualCondition{
					// Optional ID helpful when we need to identify the exact Condition that failed
					// when checking the access.
					ID: "isOwner",
					// First value to compare.
					Left: &restrict.ValueDescriptor{
						Source: restrict.ResourceField,
						Field:  "CreatedBy",
					},
					// Second value to compare.
					Right: &restrict.ValueDescriptor{
						Source: restrict.SubjectField,
						Field:  "ID",
					},
				},
			},
		},
	},
}

Access Request

AccessRequest is an object describing a question about the access - can Subject perform given Actions on particular Resource.

If you only need RBAC-like functionality, or you want to perform "preliminary" access check (for example to just check if Subject can read given Resource at all, regardless of Conditions), empty Subject/Resource instances will be enough. Otherwise, Subject and Resource should be retrieved prior to authorization and passed along in AccessRequest.

For example, in typical backend application, Subject (User) will likely come from request context. Resource (Conversation) can be fetched from the database. Entire authorization process, along with Subject/Resource retrieval, could take place in middleware function.

Here is an example of AccessRequest:

// ... manager setup

// Create empty instances or provide the correct entitites.
user := &User{}
conversation := &Conversation{}

accessRequest := &restrict.AccessRequest{
	// Required - who wants to perform the actions. It has to be
	// an instance of Subject interface. 
	Subject: user,
	// Required - on which Resource actions will be performed. It has to be
	// an instance of Resource interface.
	Resource: conversation,
	// Required - operations that given Subject wants to perform.
	Actions:  []string{"read", "create"},
	// Optional - a map of additional, external values that can be
	// accessed by Conditions. Values can be of any type.
	Context: restrict.Context{
		"SomeField": "someValue",
	},
	// Optional, lets you to skip Conditions checking.
	// Default: false.
	SkipConditions: false,
}

// If the access is granted, err will be nil - otherwise,
// an error will be returned containing an information about the failure.
err = manager.Authorize(accessRequest)

Alternatively to empty Subject/Resource instances, there are two helper functions - UseSubject() and UseResource(), that can be useful when you don't want to create empty instances or given Subject/Resource in not represented by any type in your domain. In this case, you can use:

accessRequest := &restrict.AccessRequest{
	Subject:  restrict.UseSubject("User"),
	Resource: restrict.UseResource("Conversation"),
	Actions:  []string{"read", "create"},
}

Access Manager

AccessManager is responsible for the actual validation. Once set up with proper PolicyManager instance (see PolicyManager and persistence for details), you can use its Authorize method in order to check given AccessRequest. Authorize returns an error if access is not granted, and nil otherwise (meaning there is no error and the access is granted).

var policy = &restrict.PolicyDefinition{
	// ... policy details
}

adapter := adapters.NewInMemoryAdapter(policy)
policyMananger, err := restrict.NewPolicyManager(adapter, true)
if err != nil {
	log.Fatal(err)
}

manager := restrict.NewAccessManager(policyMananger)

accessRequest := &restrict.AccessRequest{
	// ... request details
}

// 
err := manager.Authorize(accessRequest)
AccessManager errors

Since Authorize method depends on various operations, including external ones provided in a form of Conditions, its return type is a general error type. However, in order to provide easier error handling, when error is caused by policy validation only (i.e. Permission is not granted for given Role or Conditions were not satsified), Authorize returns an instance of AccessDeniedError, which has couple of helper methods.


err := manager.Authorize(accessRequest)

if accessError, ok := err.(*restrict.AccessDeniedError); ok {
	// Error() implementation. Returns a message in a form:
	// Access denied for action: "...". Reason: Permission for action: "..." is not granted for Resource: "..."
	fmt.Print(accessError)
	// Returns an AccessRequest that failed.
	fmt.Print(accessError.FailedRequest())
	// Returns underlying error which was the reason of a failure.
	fmt.Print(accessError.Reason())

	// If the reason of an AccessDeniedError was failed Condition,
	// this helper method returns it directly. Otherwise, nil will be returned.
	failedCondition := accessError.FailedCondition()

	// You can later cast the Condition to the type you want.
	if emptyCondition, ok := failedCondition.(*restrict.EmptyCondition); failedCondition != nil && ok {
		fmt.Print(emptyCondition.ID)
	}
}

Conditions

Conditions allows to define more specific access control. It's similar to ABAC model, where more than just associated role needs to be validated in order to grant the access. For example, a Subject can only update the Resource if it was created by it. Such a requirement can be expressed with Restrict as a Condition. If the Condition is not satsfied, access will not be granted, even if Subject does have the required Role.

Restrict ships with couple of built-in Conditions, but any number of custom Conditions can be added.

Built-in Conditions
Equal Condition

EqualCondition and NotEqualCondition allow to check if two values, described by ValueDescriptors, are equal or not.

&restrict.Permission{
	Action: "update",
	Conditions: restrict.Conditions{
		&restrict.EqualCondition{ // or &restrict.NotEqualCondition
			ID: "isOwner",
			// First value to compare.
			Left: &restrict.ValueDescriptor{
				Source: restrict.ResourceField,
				Field:  "CreatedBy",
			},
			// Second value to compare.
			Right: &restrict.ValueDescriptor{
				Source: restrict.SubjectField,
				Field:  "ID",
			},
		},
	},
},
Empty Condition

EmptyCondition and NotEmptyCondition allow to check if value described by ValueDescriptor is empty (not defined) or not empty (defined).

&restrict.Permission{
	// An action that given Permission allows to perform.
	Action: "delete",
	// Optional Conditions that when defined, need to be satisfied in order
	// to allow the access.
	Conditions: restrict.Conditions{
		&restrict.EmptyCondition{ // or &restrict.NotEmptyCondition
			// Optional - helps with identifying failing Condition when checking an error
			// returned from .Authorized method.
			ID: "deleteActive",
			// Value to be checked.
			Value: &restrict.ValueDescriptor{
				Source: restrict.ResourceField,
				Field:  "Active",
			},
		},
	},
}
Value Descriptor

ValueDescriptor is an object describing the value that needs to be retrieved from AccessRequest and tested by given Condition. ValueDescriptor allows to check various attributes without coupling your domain's entities to the library itself or forcing you to implement arbitrary interfaces. It uses reflection to get needed values.

ValueDescriptor needs to define value's source, which can be one of the predefined ValueSource enum type: SubjectField, ResourceField, ContextField or Explicit, and Field or Value, based on chosen source.

type exampleCondition struct {
	ValueFromSubject *restrict.ValueDescriptor
	ValueFromResource *restrict.ValueDescriptor
	ValueFromContext *restrict.ValueDescriptor
	ExplicitValue *restrict.ValueDescriptor
}

condition := &exampleCondition{
	// This value will be taken from Subject's "SomeField" passed in AccessRequest.
	ValueFromSubject: &restrict.ValueDescriptor{
		// Required, ValueSource enum.
		Source: restrict.SubjectField,
		// Optional, string.
		Field: "SomeField",
	},
	// This value will be taken from Resource's "SomeField" passed in AccessRequest.
	ValueFromResource: &restrict.ValueDescriptor{
		Source: restrict.ResourceField,
		Field: "SomeField",
	},
	// This value will be taken from Context's "SomeField" passed in AccessRequest.
	ValueFromContext: &restrict.ValueDescriptor{
		Source: restrict.ContextField,
		Field: "SomeField",
	},
	// This value will be set explicitly to 10 - please note that we are using "Value"
	// instead of "Field" here. "Value" can be of any type.
	ExplicitValue: &restrict.ValueDescriptor{
		Source: restrict.Explicit,
		// Optional, interface{}.
		Value: 10,
	},
}
Composition

Conditions can be composed in various ways, adding some flexibility to your policy. Let's consider following example:

&restrict.Permission{
	Action: "delete",
	Conditions: restrict.Conditions{
		&restrict.EmptyCondition{
			ID: "ConditionOne",
			// ... Conditions details
		},
		&restrict.NotEmptyCondition{
			ID: "ConditionTwo",
			// ... Conditions details
		},
		&restrict.EqualCondition{
			ID: "ConditionThree",
			// ... Conditions details
		},
	},
}

In this case, 3 different Conditions need to be satisfied in order to grant permission for "delete" action. This way of defining Conditions works as AND operator - if just one of them fails, permission is not granted.

But we could also define Conditions like so:

&restrict.Permission{
	Action: "delete",
	Conditions: restrict.Conditions{
		&restrict.EmptyCondition{
			ID: "ConditionOne",
			// ... Conditions details
		},
	},
},
&restrict.Permission{
	Action: "delete",
	Conditions: restrict.Conditions{
		&restrict.NotEmptyCondition{
			ID: "ConditionTwo",
			// ... Conditions details
		},
	},
},
&restrict.Permission{
	Action: "delete",
	Conditions: restrict.Conditions{
		&restrict.EqualCondition{
			ID: "ConditionThree",
			// ... Conditions details
		},
	},
}

We have 3 different Permissions with the same name but different sets of Conditions, effectively making it an OR operation - just one set of the Conditions needs to be satisfied in order to grant permission for "delete" action.

Custom Conditions

You can add any number of Conditions to match requirements of your access policy. Condition needs to implement Condition interface:

type Condition interface {
	// Type - returns Condition's type. Type needs to be unique
	// amongst other Conditions.
	Type() string

	// Check - returns true if Condition is satisfied by
	// given AccessRequest, false otherwise.
	Check(request *AccessRequest) error
}

Please note that if you want to get AccessDeniedError from Authorize method when your custom Condition is not satisfied, you should return ConditionNotSatisfiedError - any other error will be treated like internal error and will be returned directly. You can use NewConditionNotSatisfiedError to create a new instance if it.

Sticking to previous examples with User and Conversation, we can consider a case where we want to allow the User to read a Conversation only if it participates in it. Such a Condition could look like this:

// Type is spelled with upper case - it's not necessary, but built-in Conditions
// follow this convention, to make a distinction between Condition type and other
// tokens, like preset or role name.
const hasUserConditionType = "BELONGS_TO"

type hasUserCondition struct{}

func (c *hasUserCondition) Type() string {
	return hasUserConditionType
}

func (c *hasUserCondition) Check(request *restrict.AccessRequest) error {
	user, ok := request.Subject.(*User)
	if !ok {
		return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Subject has to be a User"))
	}

	conversation, ok := request.Resource.(*Conversation)
	if !ok {
		return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Resource has to be a Conversation"))
	}

	for _, userId := range conversation.Participants {
		if userId == user.ID {
			return nil
		}
	}

	return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("User does not belong to Conversation with ID: %s", conversation.ID))
}

// ... policy definition - "User" -> "Grants"
ConversationResource: {
	// ... other permissions
	&restrict.Permission{
		Action: "read",
		Conditions: restrict.Conditions{
			&hasUserCondition{},
		},
	},
},

// ... check
user := &User{ID: "user-one"}
conversation := &Conversation{Participants: []string{"user-one"}}

err := manager.Authorize(&restrict.AccessRequest{
	Subject:  user,
	Resource: conversation,
	Actions:  []string{"read"},
})
// err is nil - "user-one" belongs to conversation's Participants slice.

Or you could want to allow to delete a Conversation only when it has less than 100 messages. In this case, you could create more generalized Condition, using ValueDescriptor, and pass Max value via Context:

const greatherThanType = "GREATER_THAN"

type greaterThanCondition struct {
	// Please note that this field needs to have json/yaml tags if
	// you are using JSON/YAML based persistence.
	Value *restrict.ValueDescriptor `json:"value" yaml:"value"`
}

func (c *greaterThanCondition) Type() string {
	return greatherThanType
}

func (c *greaterThanCondition) Check(request *restrict.AccessRequest) error {
	value, err := c.Value.GetValue(request)
	if err != nil {
		return err
	}

	intValue, ok := value.(int)
	if !ok {
		return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Value has to be an integer"))
	}

	intMax, ok := request.Context["Max"].(int)
	if !ok {
		return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Max has to be an integer"))
	}

	if intValue > intMax {
		return restrict.NewConditionNotSatisfiedError(c, request, fmt.Errorf("Value is greater than max"))
	}

	return nil
}

// ... policy definition
ConversationResource: {
	// ... other permissions
	&restrict.Permission{
		Action: "delete",
		Conditions: restrict.Conditions{
			&greaterThanCondition{
				Value: &restrict.ValueDescriptor{
					Source: restrict.ResourceField,
					Field:  "MessagesCount",
				},
			},
		},
	},
},

// ... check
user := &User{}
conversation := &Conversation{MessagesCount: 90}

err = manager.Authorize(&restrict.AccessRequest{
	Subject:  user,
	Resource: conversation,
	Actions:  []string{"update"},
	Context: restrict.Context{
		"Max": 100,
	},
})
// err is nil - conversation has less than 100 messages.

You could also provide Max value as explicit value (see Value Descriptor section) and set it in your PolicyDefinition.

All of the checking logic is up to you - restrict only provides some building blocks and ensures that your Conditions will be used as specified in your policy.

Presets

Preset is simply a Permission with unique name, that you can reuse across your PolicyDefinition. The main reason behind introducing presets is saving the necessity of defining the same Conditions for different Permissions, as in many cases the same action will have identical Conditions for various Resources.

Let's consider following example:

var policy = &restrict.PolicyDefinition{
	Roles: restrict.Roles{
		"User": {
			Grants: restrict.GrantsMap{
				"Conversation": {
					&restrict.Permission{Preset: "updateOwn"},
				},
				"Message": {
					&restrict.Permission{Preset: "updateOwn"},
				}
			},
		},
	},
	PermissionPresets: restrict.PermissionPresets{
		"updateOwn": &restrict.Permission{
			Action: "update",
			Conditions: restrict.Conditions{
				&restrict.EqualCondition{
					// ... condition details
				},
			},
		},
	},
}

In this case, we can express that User can update only its own Conversation or Message, without the need for repeating Conditions definition.

But what in case we need the same Conditions, but for different actions? We can just define an action name of Permission itself, along the preset:

var policy = &restrict.PolicyDefinition{
	Roles: restrict.Roles{
		"User": {
			Grants: restrict.GrantsMap{
				"Conversation": {
					&restrict.Permission{
						Action: "update",
						Preset: "accessOwn",
					},
					&restrict.Permission{
						Action: "delete",
						Preset: "accessOwn",
					},
				},
				"Message": {
					&restrict.Permission{
						Action: "update",
						Preset: "accessOwn",
					},
					&restrict.Permission{
						Action: "delete",
						Preset: "accessOwn",
					},
				},
			},
		},
	},
	PermissionPresets: restrict.PermissionPresets{
		// Note that this preset does not have an Action anymore,
		// but it can - Permission's Action just overrides preset's Action.
		"accessOwn": &restrict.Permission{
			Conditions: restrict.Conditions{
				&restrict.EqualCondition{
					// ... condition details
				},
			},
		},
	},
}

Now we can reuse the same Conditions for different actions.

PolicyManager and persistence

PolicyManager provides thread-safe, runtime policy management, that allows to easily retrieve and manipulate your policy. It is used by AccessManager to retrieve Permissions for given role when checking AccessRequest. You can create PolicyManager like so:

myStorageAdapter := // ... create adapter 

// Second argument let's you set auto-update feature. If set to true,
// any change made via PolicyManager will be automatically saved with StorageAdapter.
// You can later disable/enable auto-update with DisableAutoUpdate() and EnableAutoUpdate() methods.
policyManager, err := restrict.NewPolicyManager(myStorageAdapter, true)
Storage adapter

PolicyManager relies on StorageAdapter instance, which is an entity providing perstistence logic for PolicyDefinition. Restrict ships with two built-in, ready to go StorageAdapters, but you can easily provide your own, by implementing following interface:

type StorageAdapter interface {
	// LoadPolicy - loads and returns PolicyDefinition from underlying
	// storage provider.
	LoadPolicy() (*PolicyDefinition, error)

	// SavePolicy - saves PolicyDefinition in underlying storage provider.
	SavePolicy(policy *PolicyDefinition) error
}

All of the Restrict's models are JSON and YAML compliant, so you can marshal/unmarshal PolicyDefinition in those formats.

Built-in Adapters
InMemoryAdapter

Simple, in-memory storage for PolicyDefinition. You can create and use it like so:

inMemoryAdapter := adapters.NewInMemoryAdapter(policy)

policyManager, err := restrict.NewPolicyManager(inMemoryAdapter, true)

InMemoryAdapter will keep PolicyDefinition object directly in memory. Using InMemoryAdapter, you will propably keep your PolicyDefinition in .go files. Please note that when using InMemoryAdapter, calling inMemoryAdapter.SavePolicy(policy) does NOT save it permanently, therefore any changes you've made with PolicyManager will be lost once program exits.

FileAdapter

FileAdapter uses file system to persit the PolicyDefinition. You can use JSON or YAML files. Here is how to use it:

fileAdapter := adapters.NewFileAdapter("filename.json", adapters.JSONFile)
// alternatively, to use YAML file:
fileAdapter := adapters.NewFileAdapter("filename.yml", adapters.YAMLFile)

policyManager, err := restrict.NewPolicyManager(fileAdapter, true)

FileAdapter will load the PolicyDefinition from given file, and keep it in sync with any changes when calling fileAdapter.SavePolicy(policy). You can also easily trasform your in-memory PolicyDefinition into JSON/YAML one, like so:

policy := &restrict.PolicyDefinition{
	// ... policy details
}

// assuming "filename.json" file does not exist or is empty
fileAdapter := adapters.NewFileAdapter("filename.json", adapters.JSONFile)

err := fileAdapter.SavePolicy(policy)
if err != nil {
	// ... error handling
}

Please refer to:

To see examples of JSON/YAML policies.

Policy management

PolicyManager provides a set of methods that will help you manage your policy in a dynamic way. You can manipulate it in runtime, or create custom tools in order to add and remove Roles, grant and revoke Permissions or manage presets. Full list of PolicyManager's methods can be found here:

PolicyManager docs

Examples

Middleware function

You can easily use Restrict as a middleware function in your backend application. Here is an example of using Restrict inside Gin framework's handler function:

func WithAuth(
	actions []string,
	resource restrict.Resource,
) gin.HandlerFunc {
	return func(c *gin.Context) {
		user, err := authenticateUser(c)
		if err != nil {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}

		c.Set(UserContextKey, user)

		// If any previous handler fetched concrete Resource, we want to
		// override it here, so we can test it against Conditions if necessary.
		if contextResource, ok := c.Get(ResourceContextKey); ok {
			if res, ok := contextResource.(restrict.Resource); ok {
				resource = res
			}
		}

		err = accessManager.Authorize(&restrict.AccessRequest{
			Subject:  user,
			Resource: resource,
			Actions:  actions,
		})

		// If error is related to insufficient privileges, we want to
		// return appropriate response.
		if accessError, ok := err.(*restrict.AccessDeniedError); ok {
			log.Print(accessError)

			c.AbortWithError(http.StatusForbidden, accessError)
			return
		}

		// If not, some other error occurred.
		if err != nil {
			c.AbortWithStatus(http.StatusInternalServerError)
			return
		}
	}
}

You can see entire working example HERE

Roadmap

Check this Project for upcoming features.

Development

Prerequisites
  1. Install golangci-lint
  2. Set your IDE to use golangci-lint (instructions)
  3. Install python3
  4. Run git config core.hooksPath .githooks to wire up project's git hooks
Conventions

This repository follows ConventionalCommits specification for creating commit messages. There is prepare-commit-msg hook set up to ensure following those rules. Branch names should also reflect the type of work it contains - one of following should be used:

  • feature/<task-description>
  • bugfix/<task-description>
  • chore/<task-description>

Documentation

Overview

Package restrict provides an authorization library, with a hybrid of RBAC and ABAC models.

Index

Constants

View Source
const (
	// EmptyConditionType - EmptyCondition's type identifier.
	EmptyConditionType = "EMPTY"
	// NotEmptyConditionType - NotEmptyCondition's type identifier.
	NotEmptyConditionType = "NOT_EMPTY"
)
View Source
const (
	// EqualConditionType - EqualCondition's type identifier.
	EqualConditionType = "EQUAL"
	//NotEqualConditionType - NotEqualCondition's type identifier.
	NotEqualConditionType = "NOT_EQUAL"
)

Variables

View Source
var ConditionFactories = ConditionFatoriesMap{
	EqualConditionType: func() Condition {
		return new(EqualCondition)
	},
	NotEqualConditionType: func() Condition {
		return new(NotEqualCondition)
	},
	EmptyConditionType: func() Condition {
		return new(EmptyCondition)
	},
	NotEmptyConditionType: func() Condition {
		return new(NotEmptyCondition)
	},
}

ConditionFactories - stores a map of functions responsible for creating new Conditions, based on their names.

Functions

func RegisterConditionFactory

func RegisterConditionFactory(name string, factory ConditionFactory) error

RegisterConditionFactory - adds a new ConditionFactory under given name. If given name is already taken, an error is returned.

func UseResource

func UseResource(name string) *baseResource

UseResource - returns baseResource instance.

func UseSubject

func UseSubject(role string) *baseSubject

UseSubject - returns baseSubject instance.

Types

type AccessDeniedError

type AccessDeniedError struct {
	// contains filtered or unexported fields
}

AccessDeniedError - thrown when AccessRequest could not be satisfied due to insufficient privileges.

func (*AccessDeniedError) Error

func (e *AccessDeniedError) Error() string

Error - error interface implementation.

func (*AccessDeniedError) FailedCondition

func (e *AccessDeniedError) FailedCondition() Condition

FailedCondition - helper function for retrieving underlying failed Condition.

func (*AccessDeniedError) FailedRequest

func (e *AccessDeniedError) FailedRequest() *AccessRequest

FailedRequest - returns an AccessRequest for which access has been denied.

func (*AccessDeniedError) Reason

func (e *AccessDeniedError) Reason() error

Reason - returns underlying reason (an error) for denying the access.

type AccessManager

type AccessManager struct {
	// contains filtered or unexported fields
}

AccessManager - an entity responsible for checking the authorization. It uses underlying PolicyProvider to test an AccessRequest against currently used PolicyDefinition.

func NewAccessManager

func NewAccessManager(policyManager PolicyProvider) *AccessManager

NewAccessManager - returns new AccessManager instance.

func (*AccessManager) Authorize

func (am *AccessManager) Authorize(request *AccessRequest) error

Authorize - checks if given AccessRequest can be satisfied given currently loaded policy. Returns an error if access is not granted or any other problem occurred, nil otherwise.

type AccessRequest

type AccessRequest struct {
	// Subject - subject (typically a user) that wants to perform given Actions.
	// Needs to implement Subject interface.
	Subject Subject
	// Resource - resource that given Subject wants to interact with.
	// Needs to implement Resource interface.
	Resource Resource
	// Actions - list of operations Subject wants to perform on given Resource.
	Actions []string
	// Context - map of any additional values needed while checking the access.
	Context Context
	// SkipConditions - allows to skip Conditions while checking the access.
	SkipConditions bool
}

AccessRequest - describes a Subject's intention to perform some Actions against given Resource.

type Condition

type Condition interface {
	// Type - returns Condition's type.
	Type() string

	// Check - returns true if Condition is satisfied by
	// given AccessRequest, false otherwise.
	Check(request *AccessRequest) error
}

Condition - additional requirement that needs to be satisfied to grant given permission.

type ConditionFactory

type ConditionFactory func() Condition

ConditionFactory - factory function for Condition.

type ConditionFactoryAlreadyExistsError

type ConditionFactoryAlreadyExistsError struct {
	// contains filtered or unexported fields
}

ConditionFactoryAlreadyExistsError - thrown when ConditionFactory is being added under a name that's already set in ConditionFactories map.

func (*ConditionFactoryAlreadyExistsError) Error

Error - error interface implementation.

type ConditionFactoryNotFoundError

type ConditionFactoryNotFoundError struct {
	// contains filtered or unexported fields
}

ConditionFactoryNotFoundError - thrown when ConditionFactory is not found while unmarshaling a Permission.

func (*ConditionFactoryNotFoundError) Error

Error - error interface implementation.

type ConditionFatoriesMap

type ConditionFatoriesMap = map[string]ConditionFactory

ConditionFatoriesMap - map of Condition factories.

type ConditionNotSatisfiedError

type ConditionNotSatisfiedError struct {
	// contains filtered or unexported fields
}

ConditionNotSatisfiedError - thrown when given Condition was not satisfied due to insufficient privileges for given AccessRequest.

func NewConditionNotSatisfiedError

func NewConditionNotSatisfiedError(condition Condition, request *AccessRequest, reason error) *ConditionNotSatisfiedError

NewConditionNotSatisfiedError - returns new ConditionNotSatisfiedError instance.

func (*ConditionNotSatisfiedError) Error

Error - error interface implementation.

func (*ConditionNotSatisfiedError) FailedCondition

func (e *ConditionNotSatisfiedError) FailedCondition() Condition

FailedCondition - returns failed Condition.

func (*ConditionNotSatisfiedError) FailedRequest

func (e *ConditionNotSatisfiedError) FailedRequest() *AccessRequest

FailedRequest - returns failed AccessRequest.

func (*ConditionNotSatisfiedError) Reason

func (e *ConditionNotSatisfiedError) Reason() error

Reason - returns underlying reason (an error) of failing Condition.

type Conditions

type Conditions []Condition

Conditions - alias type for Conditions array.

func (Conditions) MarshalJSON

func (cs Conditions) MarshalJSON() ([]byte, error)

MarshalJSON - marshals a map of Conditions to JSON data.

func (Conditions) MarshalYAML

func (cs Conditions) MarshalYAML() (interface{}, error)

MarshalYAML - marshals a map of Conditions to YAML data.

func (*Conditions) UnmarshalJSON

func (cs *Conditions) UnmarshalJSON(jsonData []byte) error

UnmarshalJSON - unmarshals a JSON-coded map of Conditions.

func (*Conditions) UnmarshalYAML

func (cs *Conditions) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML - unmarshals a YAML-coded map of Conditions.

type Context

type Context map[string]interface{}

Context - alias type for a map of any values.

type EmptyCondition

type EmptyCondition baseEmptyCondition

EmptyCondition - Condition for testing whether given value is empty.

func (*EmptyCondition) Check

func (c *EmptyCondition) Check(request *AccessRequest) error

Check - returns true if value is empty (zero-like), false otherwise.

func (*EmptyCondition) Type

func (c *EmptyCondition) Type() string

Type - returns Condition's type.

type EqualCondition

type EqualCondition baseEqualCondition

EqualCondition - checks whether given value (Left) is equal to some other value (Right).

func (*EqualCondition) Check

func (c *EqualCondition) Check(request *AccessRequest) error

Check - returns true if values are equal, false otherwise.

func (*EqualCondition) Type

func (c *EqualCondition) Type() string

Type - returns Condition's type.

type GrantsMap

type GrantsMap map[string]Permissions

GrantsMap - alias type for map of Permission slices.

type NotEmptyCondition

type NotEmptyCondition baseEmptyCondition

func (*NotEmptyCondition) Check

func (c *NotEmptyCondition) Check(request *AccessRequest) error

Check - returns true if value is not empty (zero-like), false otherwise.

func (*NotEmptyCondition) Type

func (c *NotEmptyCondition) Type() string

Type - returns Condition's type.

type NotEqualCondition

type NotEqualCondition baseEqualCondition

EqualCondition - checks whether given value (Left) is not equal to some other value (Right).

func (*NotEqualCondition) Check

func (c *NotEqualCondition) Check(request *AccessRequest) error

Check - returns true if values are not equal, false otherwise.

func (*NotEqualCondition) Type

func (c *NotEqualCondition) Type() string

Type - returns Condition's type.

type Permission

type Permission struct {
	// Action that will be allowed to perform if the Permission is granted, and Conditions
	// are satisfied.
	Action string `json:"action,omitempty" yaml:"action,omitempty"`
	// Conditions that need to be satisfied in order to allow the subject perform given Action.
	Conditions Conditions `json:"conditions,omitempty" yaml:"conditions,omitempty"`
	// Preset allows to extend Permission defined in PolicyDefinition.
	Preset string `json:"preset,omitempty" yaml:"preset,omitempty"`
}

Permission - describes an Action that can be performed in regards to some Resource, with specified Conditions.

type PermissionNotGrantedError

type PermissionNotGrantedError struct {
	// contains filtered or unexported fields
}

PermissionNotGrantedError - thrown when Permission grant for action was not found for given Resource.

func (*PermissionNotGrantedError) Error

func (e *PermissionNotGrantedError) Error() string

Error - error interface implementation.

type PermissionPresetAlreadyExistsError

type PermissionPresetAlreadyExistsError struct {
	// contains filtered or unexported fields
}

PermissionPresetAlreadyExistsError - thrown when a new Permission preset is being added with a name (key) that already exists.

func (*PermissionPresetAlreadyExistsError) Error

type PermissionPresetNotFoundError

type PermissionPresetNotFoundError struct {
	// contains filtered or unexported fields
}

PermissionPresetNotFoundError - thrown when Permission specifies a preset which is not defined in PermissionPresets on PolicyDefinition.

func (*PermissionPresetNotFoundError) Error

Error - error interface implementation.

type PermissionPresets

type PermissionPresets map[string]*Permission

PermissionPresets - a map of reusable Permissions. Map key serves as a preset's name, that can be later referenced by Permission. Presets are applied when policy is loaded.

type Permissions

type Permissions []*Permission

Permissions - alias type for slice of Permissions.

type PolicyDefinition

type PolicyDefinition struct {
	// PermissionPresets - a map of Permission presets.
	PermissionPresets PermissionPresets `json:"permissionPresets,omitempty" yaml:"permissionPresets,omitempty"`
	// Roles - collection of Roles used in the domain.
	Roles Roles `json:"roles" yaml:"roles"`
}

PolicyDefinition - describes a model of Roles and Permissions that are defined for the domain.

type PolicyManager

type PolicyManager struct {

	// PolicyManager should thread-safe for writing operations, therefore it uses RWMutex.
	sync.RWMutex
	// contains filtered or unexported fields
}

PolicyManager - an entity responsible for managing PolicyDefinition. It uses passed StorageAdapter for policy persistence.

func NewPolicyManager

func NewPolicyManager(adapter StorageAdapter, autoUpdate bool) (*PolicyManager, error)

NewPolicyManager - returns new PolicyManager instance and loads PolicyDefinition using passed StorageAdapter.

func (*PolicyManager) AddPermission

func (pm *PolicyManager) AddPermission(roleID, resourceID string, permission *Permission) error

AddPermission - adds a new Permission for the Role and Resource with passed ids. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) AddPermissionPreset

func (pm *PolicyManager) AddPermissionPreset(name string, preset *Permission) error

AddPermissionPreset - adds new Permission preset to PolicyDefinition. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) AddRole

func (pm *PolicyManager) AddRole(role *Role) error

AddRole - adds a new role to the policy. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) DeletePermission

func (pm *PolicyManager) DeletePermission(roleID, resourceID, action string) error

DeletePermission - removes a Permission with given name for Role and Resource with passed ids. Please note that deleting a Permission for given action will revoke ALL of the Permissions that share this action. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) DeletePermissionPreset

func (pm *PolicyManager) DeletePermissionPreset(name string) error

DeletePermissionPreset - removes Permission preset with given name. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) DeleteRole

func (pm *PolicyManager) DeleteRole(roleID string) error

DeleteRole - removes a Role with given ID. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) DisableAutoUpdate

func (pm *PolicyManager) DisableAutoUpdate()

DisableAutoUpdate - disables automatic update.

func (*PolicyManager) EnableAutoUpdate

func (pm *PolicyManager) EnableAutoUpdate()

EnableAutoUpdate - enables automatic update.

func (*PolicyManager) GetPolicy

func (pm *PolicyManager) GetPolicy() *PolicyDefinition

GetPolicy - returns currently loaded PolicyDefinition.

func (*PolicyManager) GetRole

func (pm *PolicyManager) GetRole(roleID string) (*Role, error)

GetRole - returns a Role with given ID from currently loaded PolicyDefiniton.

func (*PolicyManager) LoadPolicy

func (pm *PolicyManager) LoadPolicy() error

LoadPolicy - proxy method for loading the policy via StorageAdapter set when creating PolicyManager instance. Calling this method will override currently loaded policy.

func (*PolicyManager) SavePolicy

func (pm *PolicyManager) SavePolicy() error

SavePolicy - proxy method for saving the policy via StorageAdapter set when creating PolicyManager instance.

func (*PolicyManager) UpdatePermissionPreset

func (pm *PolicyManager) UpdatePermissionPreset(name string, preset *Permission) error

UpdatePermissionPreset - updates a Permission preset in PolicyDefinition. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) UpdateRole

func (pm *PolicyManager) UpdateRole(role *Role) error

UpdateRole - updates existing Role in currently loaded policy. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) UpsertPermissionPreset

func (pm *PolicyManager) UpsertPermissionPreset(name string, preset *Permission) error

UpsertPermissionPreset - updates Permission preset if exists, adds a new otherwise. Saves with StorageAdapter if autoUpdate is set to true.

func (*PolicyManager) UpsertRole

func (pm *PolicyManager) UpsertRole(role *Role) error

UpsertRole - updates a Role if exists, adds new Role otherwise. Saves with StorageAdapter if autoUpdate is set to true.

type PolicyProvider

type PolicyProvider interface {
	GetRole(roleID string) (*Role, error)
}

PolicyProvider - interface for an entity that will provide Role configuration for AccessProvider.

type RequestMalformedError

type RequestMalformedError struct {
	// contains filtered or unexported fields
}

RequestMalformedError - thrown when AccessRequest is not correct or does not contain all necessary information.

func (*RequestMalformedError) Error

func (e *RequestMalformedError) Error() string

Error - error interface implementation.

func (*RequestMalformedError) FailedRequest

func (e *RequestMalformedError) FailedRequest() *AccessRequest

FailedRequest - returns an AccessRequest for which access has been denied.

func (*RequestMalformedError) Reason

func (e *RequestMalformedError) Reason() error

Reason - returns underlying reason (an error) of malformed Request.

type Resource

type Resource interface {
	// GetResourceName - returns a Resource's name. Should be the same as the one
	// used in PolicyDefinition.
	GetResourceName() string
}

Resource - interface that needs to be implemented by any entity which acts as a resource in the system.

type Role

type Role struct {
	// ID - unique identifier of the Role.
	ID string `json:"-" yaml:"-"`
	// Description - optional description for a Role.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Grants - contains sets of Permissions assigned to Resources.
	Grants GrantsMap `json:"grants" yaml:"grants"`
	// Parents - other Roles that given Role inherits from. If a Permission is granted
	// for a parent, it is also granted for a child.
	Parents []string `json:"parents,omitempty" yaml:"parents,omitempty"`
}

Role - describes privileges of a Role's members.

type RoleAlreadyExistsError

type RoleAlreadyExistsError struct {
	// contains filtered or unexported fields
}

RoleAlreadyExistsError - thrown when new Role is being added with ID that already exists in the PolicyDefinition.

func (*RoleAlreadyExistsError) Error

func (e *RoleAlreadyExistsError) Error() string

Error - error interface implementation.

type RoleInheritanceCycleError

type RoleInheritanceCycleError struct {
	// contains filtered or unexported fields
}

RoleInheritanceCycleError - thrown when circular Role inheritance is detected.

func (*RoleInheritanceCycleError) Error

func (e *RoleInheritanceCycleError) Error() string

Error - error interface implementation.

type RoleNotFoundError

type RoleNotFoundError struct {
	// contains filtered or unexported fields
}

RoleNotFoundError - thrown when there is an operation called for a Role that does not exist.

func (*RoleNotFoundError) Error

func (e *RoleNotFoundError) Error() string

Error - error interface implementation.

type Roles

type Roles map[string]*Role

Roles - alias type for map of Roles.

func (*Roles) UnmarshalJSON

func (rs *Roles) UnmarshalJSON(jsonData []byte) error

UnmarshalJSON - unmarshals a JSON-coded map of Roles.

func (*Roles) UnmarshalYAML

func (rs *Roles) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML - unmarshals a YAML-coded map of Roles.

type StorageAdapter

type StorageAdapter interface {
	// LoadPolicy - loads and returns PolicyDefinition from underlying
	// storage provider.
	LoadPolicy() (*PolicyDefinition, error)

	// SavePolicy - saves PolicyDefinition in underlying storage provider.
	SavePolicy(policy *PolicyDefinition) error
}

StorageAdapter - interface for an entity that will provide persistence logic for PolicyDefinition.

type Subject

type Subject interface {
	// GetRole - returns a Subject's role.
	GetRole() string
}

Subject - interface that has to be implemented by any entity which authorization needs to be checked.

type ValueDescriptor

type ValueDescriptor struct {
	// Source - source of the value, one of the predefined enum type (ValueSource).
	Source ValueSource `json:"source,omitempty" yaml:"source,omitempty"`
	// Field - field on the given ValueSource that should hold the value.
	Field string `json:"field,omitempty" yaml:"field,omitempty"`
	// Value - explicit value taken when using ValueSource.Explicit as value source.
	Value interface{} `json:"value,omitempty" yaml:"value,omitempty"`
}

ValueDescriptor - describes a value that will be tested in its parent Condition.

func (*ValueDescriptor) GetValue

func (vd *ValueDescriptor) GetValue(request *AccessRequest) (interface{}, error)

GetValue - returns real value represented by given ValueDescriptor.

type ValueDescriptorMalformedError

type ValueDescriptorMalformedError struct {
	// contains filtered or unexported fields
}

ValueDescriptorMalformedError - thrown when malformed ValueDescriptor is being resolved.

func (*ValueDescriptorMalformedError) Error

Error - error interface implementation.

func (*ValueDescriptorMalformedError) FailedDescriptor

func (e *ValueDescriptorMalformedError) FailedDescriptor() *ValueDescriptor

FailedDescriptor - returns failed ValueDescriptor.

func (*ValueDescriptorMalformedError) Reason

Reason - returns underlying reason (an error) of malformed ValueDescriptor.

type ValueSource

type ValueSource int

ValueSource - enum type for source of value for given ValueDescriptor.

const (

	// SubjectField - value that comes from Subject's field.
	SubjectField ValueSource
	// ResourceField - value that comes from Resource's field.
	ResourceField
	// ContextField - value that comes from Context's field.
	ContextField
	// Explicit - value set explicitly in PolicyDefinition.
	Explicit
)

func (ValueSource) MarshalJSON

func (vs ValueSource) MarshalJSON() ([]byte, error)

MarshalJSON - marshals a ValueSource enum into its name as string.

func (ValueSource) MarshalYAML

func (vs ValueSource) MarshalYAML() (interface{}, error)

MarshalYAML - marshals a ValueSource enum into its name as string.

func (ValueSource) String

func (vs ValueSource) String() string

String - Stringer implementation.

func (*ValueSource) UnmarshalJSON

func (vs *ValueSource) UnmarshalJSON(jsonData []byte) error

UnmarshalJSON - unmarshals a string into ValueSource.

func (*ValueSource) UnmarshalYAML

func (vs *ValueSource) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML - unmarshals a string into ValueSource.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL