can

package module
v0.0.0-...-302cb1c Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2024 License: MIT Imports: 8 Imported by: 0

README

can

[![GoDoc Widget]][GoDoc]

can provides primitives for authorization. Authorization is the way it restricts what resource a given role and permissions. Simple primitives for RBAC (Role Based Access Control) are the building blocks for can. This package was inspired by https://github.com/CanCanCommunity/cancancan

Install

go get -u github.com/acmacalister/can

Examples

See a complete HTTP application in examples/main.go.

TODO: fix up these

const (
    PermissionClients string = "permission_clients"
    UserDetailsContextKey string = "userDetailsContextKey"
)

type userDetails {
    Role *can.Role
}

type Client struct {
    ID int64 `json:"id"`
    UserID null.Int64 `json:"user_id"`
    Name string `json:"name"`
}

if !can.Can(r.Context(), userDetails.Role, can.Compare(client.UserID, userDetails.ID), PermissionClients, can.Read, nil) {
    return errors.New("unauthorized")
}

if !can.Can(r.Context(), userDetails.Role, can.Compare(client.UserID, userDetails.ID), PermissionClients, can.Read, nil) {
    return errors.New("unauthorized")
}


func CustomAuthorization(ctx context.Context, role *Role, compare func() bool, permission string, ability Ability) bool {
    return true // everything is allowed!
}

if !can.Can(r.Context(), userDetails.Role, can.Compare(client.UserID, userDetails.ID), PermissionClients, can.Read, CustomAuthorization) {
    return errors.New("unauthorized")
}

func getSomething(w http.ResponseWriter, r *http.Request) {
	userDetails, ok := r.Context().Value(UserDetailsContextKey).(UserDetail)
	if !ok {
        w.WriteHeader(http.StatusBadRequest)
        return
	}

    id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    client, err := service.ByID(id, userDetails.OrganizationID) // some sort of model or service to pull clients off of the ID.
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    if !can.Can(r.Context(), userDetails.Role, can.Compare(client.UserID, userDetails.ID), PermissionClients, can.Read, nil) {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    w.WriteHeader(http.StatusOK)
    return
}

func createSomething(w http.ResponseWriter, r *http.Request) {
	userDetails, ok := r.Context().Value(UserDetailsContextKey).(UserDetail)
	if !ok {
        w.WriteHeader(http.StatusBadRequest)
        return
	}

    var client Client
    if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

	allowed := (newClient.UserID.Valid && newClient.UserID.Int64 == userDetails.ID)

    if !can.Can(r.Context(), userDetails.Role, can.Compare(allowed, true), PermissionClients, can.Create, nil) {
        w.WriteHeader(http.StatusUnauthorized)
        return
	}


    w.WriteHeader(http.StatusOK)
    return
}

How do I store the permissions in the database?

TODO.

How do add custom abilities?

Just create new constants with the can.Ability type.

What does the default authorization look like?

Basically the DefaultCan method looks like so:

func DefaultCan(ctx context.Context, role *Role, compare func() bool, permission string, ability Ability) bool {
	if role == nil {
		return false
	}

	perm, ok := role.Permissions[permission]
	if !ok {
		return false
	}

	if _, ok := perm.Abilities[ability]; !ok {
		return false
	}

	switch ability {
	case Manage:
		return true
	case Read, ReadAll, Create, Update, Delete:
		if compare == nil {
			return false
		}
		return compare()
	}

	return false
}

Basically the manage ability allows all the abilities for the permission (useful for an "admin" type role). Otherwise, all other abilities only allow a user to access if the compare function is true. Think of the compare function as a way to check that the user ID is owned by that user. Obviously you can customize as you like, but that is a concrete example of its usage as seen above.

Details

can is designed for authorization for the "controller" or routing layer of an application. It isn't designed for views/presentation layer. Users should have permissions and every permission has abilities. You could implement this in a middleware like the authorize_and_load in the RoR version, but would either require shoving everything in a request context, using reflect, or requiring application logic specific to your application. This was a first attempt to build a simple generic authorization library for Go applications. Feel free to open issues or Pull Requests with some feedback or thoughts.

License

MIT

Documentation

Overview

Package can provides primitives for authorization. Authorization is the way it restricts what resource a given role and permissions. Simple primitives for RBAC (Role Based Access Control) are the building blocks for can. This package was inspired by https://github.com/CanCanCommunity/cancancan

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Can

func Can(ctx context.Context, role Role, permission string, ability Ability, compare func() bool) bool

Can is the heart and soul of the can package. It can take a custom compare function to do various authorization checking

ctx - a standard ctx to pass to authorization. Useful for passing additional request specific data and canceling the can function call if it was signal to a remote authorization service.

role - a role structure that contains the role and permissions to check authorization on.

permission - defines the permission to check of a given object.

ability - defines the ability to check of a given object.

compare - a simple function to check request specific data. Things like if a user can update their own comments or the like.

returns a true or false if the role or permission is allowed.

func Compare

func Compare[T Comparable](i, j T) func() bool

Compare is a helper function to easily satisfies the compare function in the main Can function

func PermissionFromPath

func PermissionFromPath(r *http.Request) string

PermissionFromPath uses the request path to build a permission that can be used to check authorization in the Can function. Uses the chi router context to build the permission.

r - a standard http request

returns - a string representation of a permission

Types

type Ability

type Ability int64

Ability provides typed constants for general resource control.

const (
	// Read is for access to a given resource
	Read Ability = iota
	// Create is for creating a given resource
	Create
	// Update is for updating a given resource
	Update
	// Delete is for deleting a given resource
	Delete
	// All is read/create/update/delete for a give resource
	All
	// Skip is for skipping authorization lookups on a given resource.
	// Useful if for options style results and when authorization might be
	// handled later in a request chain.
	Skip
	// None is useful for signaling no access to given resource. Also useful for
	// error states
	None
)

func BuildFromMethod

func BuildFromMethod(method string) Ability

BuildFromMethod uses standard Rest conventions to build a permission and ability from the request. Useful for implementing authorization middleware

method - a string representation of an HTTP verb. GET/POST/PUT, etc

returns - an ability

func StringToAbility

func StringToAbility(s string) Ability

StringToAbility converts a string to an ability type

s is a string to convert

returns an ability or -1 if the string is incorrect

func (Ability) String

func (a Ability) String() string

String implements the Stringer interface.

returns a string representation of the ability type

type CanFn

type CanFn func(ctx context.Context, role *Role, compare func() bool, permission string, ability Ability) bool

CanFn is a type for the implementing custom authorization functions.

type Comparable

type Comparable interface {
	constraints.Ordered | bool
}

type DiskPermission

type DiskPermission struct {
	Abilities []string `json:"abilities" db:"abilities" yaml:"abilities"`
	Routes    []string `json:"routes" db:"routes" yaml:"routes"`
	Resource  string   `json:"resource" db:"resource" yaml:"resource"`
}

type DiskRole

type DiskRole map[string]DiskPermission

diskRole is the private struct that represents how the roles are encoded in yaml to disk

type DiskRoles

type DiskRoles map[string]DiskRole

DiskRoles is a map of roles that are encoded in yaml

type Permission

type Permission struct {
	Abilities map[Ability]struct{} `json:"abilities" db:"abilities" yaml:"abilities"`
	Resource  string               `json:"resource" db:"resource" yaml:"resource"`
}

Permission provides typed structure for general permissions or access to a given resource. This struct is easily embedded in other types to extend the permissions (see examples).

type Role

type Role map[string]Permission

Role provides typed structure for general roles that enumerates a set of permissions. This struct is easily embedded in other types to extend the role (see examples).

type Roles

type Roles map[string]Role

func Config

func Config(c DiskRoles) Roles

Config takes a per parsed config file and return a map of Roles. Useful if the config file is a different format than yaml or if the config file is parsed elsewhere. c - a set of disk roles

returns - a map of Roles

func OpenFile

func OpenFile(filename string) (Roles, error)

OpenFile takes a yaml file and returns a map of Roles filename - yaml encoded file for parsing

returns - a map of Roles and an error

func (Roles) UnmarshalYAML

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

UnmarshalYAML implement the yaml Unmarshaler interface

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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