auth

package
v0.4.6 Latest Latest
Warning

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

Go to latest
Published: Nov 28, 2023 License: Apache-2.0 Imports: 21 Imported by: 1

README

Authorization Module

Package auth implements a ready-to-use authorization system for goes-driven apps.

This module implements an authorization system around the concepts of actors and roles, and builds on top of goes' aggregate, event, and command system.

User management is explicitly not provided by this module. Instead, it allows you to integrate your own or third-party user management using custom actors.

Features

  • Role-based authorization
  • Action-based authorization
  • Aggregate-specific authorization
  • Wildcard support

Design

Actors

An Actor represents a user within the system, which can be anything, from a real-world human to a system user to an API key. Actors are aggregates, and by default, are simply identified by their aggregate ids (UUID).

Another actor kind provided by this module is the string-Actor, which is not only identified by its aggregate id but also by some arbitrary string. For example, the user of an API key can be granted permission to perform actions through the string-Actor aggregate.

An actor can be granted an arbitrary amount of permissions. All permissions can be revoked from actors after they were granted.

Example
package example

// Grant a UUID-Actor the permission to "view" and "update" a "foo" aggregate.
func exampleUUIDActor() {
	actor := auth.NewUUIDActor(uuid.New())

	actor.Grant(aggregate.Ref{
		Name: "foo",
		ID: uuid.UUID{...},
	}, "view", "update")
}

// Grant a string-Actor the permission to "view" and "update" a "foo" aggregate.
func exampleStringActor() {
	actor := auth.NewStringActor(uuid.New())

	actor.Identify("foo-bar-baz")

	actor.Grant(aggregate.Ref{
		Name: "foo",
		ID: uuid.UUID{...},
	}, "view", "update")
}
Roles

A Role represents a group of actors that are granted permissions to perform specific actions on specific aggregates. Actors are allowed to perform an action if they were either granted the permission directly or if they are a member of a role that was granted the permission.

package example

// Grant a role the permission to "view" and "update" a "foo" aggregate.
func example() {
	role := auth.NewRole(uuid.New())

	role.Identify("admin")

	role.Grant(aggregate.Ref{
		Name: "foo",
		ID: uuid.UUID{...},
	}, "view", "update")
}
Permissions

The actual permissions of an actor cannot be queried from the Actor aggregate alone because an actor may have permissions that were granted to them through a role. In order to query the permission of an actor, the Permissions read-model must be used instead. The permission read-model projects the actor and role events to provide the actual permissions of an actor.

package example

func example(permissions auth.PermissionRepository, actorID uuid.UUID) {
	perms, err := permissions.Fetch(context.TODO(), actorID)
	// handle err

	canView := perms.Allows("view", aggregate.Ref{
		Name: "foo",
		ID: uuid.UUID{...},
	})
}

Usage

Setup

The authorization module can be integrted into an existing goes application:

package main

func main() {
	// Setup of your own application.
	var eventRegistry *codec.Registry
	var commandRegistry *codec.Registry
	var bus event.Bus
	var store event.Store
	var repo aggregate.Repository
	var cbus command.Bus

	// Authorization setup

	// Register "auth" events and commands into your registries.
	auth.RegisterEvents(eventRegistry)
	auth.RegisterCommands(commandRegistry)

	// Create "auth" repositories
	actorRepos := auth.NewActorRepositories()
	roles := auth.NewRoleRepository(repo)
	permissions := auth.InMemoryPermissionRepository()

	// Run the lookup projection in the background.
	lookup := auth.NewLookup(store, bus)
	lookupErrors, err := lookup.Run(context.TODO())
	// handle err

	// Run the permission projector in the background.
	permissionProjector := auth.NewPermissionProjector(permissions, bus, store)
	permissionErrors, err := permissionProjector.Run(context.TODO())
	// handle err

	// Handle "auth" commands in the background.
	authErrors, err := auth.HandleCommads(context.TODO(), cbus, actorRepos, roles)
	// handle err

	errs := streams.FanInAll(lookupErrors, permissionErrors, authErrors)

	// Log errors.
	for err := range errs {
		log.Println(err)
	}
}
Grant Permissions

Permissions can be granted to actors and roles. The following example grants the permissions to "view" and "update" the "foo" aggregate with the given fooID to the provided actor and role.

package example

func example(actor *auth.Actor, role *auth.Role, fooID uuid.UUID) {
	actor.Grant(aggregate.Ref{
		Name: "foo",
		ID: fooID,
	}, "view", "update")

	role.Grant(aggregate.Ref{
		Name: "foo",
		ID: fooID,
	}, "view", "update")
}

// or using the command system
func example(bus command.Bus, actorID, roleID, fooID uuid.UUID) {
	cmd := auth.GrantToActor(actorID, aggregate.Ref{...}, "view", "update")
	bus.Dispatch(context.TODO(), cmd)

	cmd := auth.GrantToRole(roleID, aggregate.Ref{...}, "view", "update")
	bus.Dispatch(context.TODO(), cmd)
}
Revoke Permissions

Permissions can also be revoked from actors and roles. The following example revokes the permissions to "view" and "update" the "foo" aggregate with the given fooID from the provided actor and role.

package example

func example(actor *auth.Actor, role *auth.Role, fooID uuid.UUID) {
	actor.Revoke(aggregate.Ref{
		Name: "foo",
		ID: fooID,
	}, "view", "update")

	role.Revoke(aggregate.Ref{
		Name: "foo",
		ID: fooID,
	}, "view", "update")
}

// or using the command system
func example(bus command.Bus, actorID, roleID, fooID uuid.UUID) {
	cmd := auth.RevokeFromActor(actorID, aggregate.Ref{...}, "view", "update")
	bus.Dispatch(context.TODO(), cmd)

	cmd := auth.RevokeFromRole(roleID, aggregate.Ref{...}, "view", "update")
	bus.Dispatch(context.TODO(), cmd)
}
HTTP Middleware

The http/middleware package implements HTTP middleware that can be used to protect routes from unauthorized access. The two main middlewares are:

  • Authorize(...) – authorizes actors
  • Permission(...) – protects routes

In order to protect a route, these two middlewares must be added to the HTTP handler. The Authorize() middleware must be called before the Permission() middleware.

A middleware Factory can be used to create middleware without having to pass a PermissionFetcher or Lookup each time:

package example

func example(perms middleware.PermissionFetcher, lookup *auth.Lookup) {
	factory := middleware.NewFactory(perms, lookup)

	authorize := factory.Authorize(func(middleware.Authorizer, *http.Request) {
		// ...
	})

	permission := factory.Permision("view", func(*http.Request) aggregate.Ref {
		// ...
	})
}
Example using go-chi
package example

func example(factory middleware.Factory) {
	r := chi.NewRouter()
	r.Use(
		factory.Authorize(func(auth middleware.Authorizer, r *http.Request) {
			var actorID uuid.UUID // extract from request
			auth.Authorize(actorID) // authorize the given actor

			// multiple actors can be authorized at the same time
			auth.Authorize(<another-actor-id>)
		}),

		factory.Permission("view", func(r *http.Request) aggregate.Ref {
			// we must return which aggregate the actor wants to "view".
			return aggregate.Ref{
				Name: "foo",
				ID: chi.URLParam(r, "FooID"),
			}
		}),
	)
}
Custom Actors

This module implements actors for two kinds of identifiers: UUIDs and strings. These two kinds should suffice for most applications but if required, your application may configure additional actor kinds.

Example
package example

const CustomActorKind = "custom"

type ActorID struct {
	Foo string
	Bar int
}

func (id ActorID) String() string {
	return fmt.Sprintf("%s_%d", id.Foo, id.Bar)
}

// NewCustomActor is the constructor of your custom actor kind.
// The provided auth.ActorConfig provides the parser and formatter for the
// actor id.
func NewCustomActor(id uuid.UUID) *auth.Actor {
	return auth.NewActor(id, auth.ActorConfig[ActorID]{
		Kind: CustomActorKind,
		FormatID: func(id ActorID) string {
			return id.String()
		},
		ParseID: func(v string) (ActorID, error) {
			parts := strings.Split(v, "_")
			if len(parts) != 2 {
				return ActorID{}, errors.New("invalid id")
			}
 
			bar, err := strconv.Atoi(parts[1])
			if err != nil {
				return ActorID{}, fmt.Errorf("parse Bar: %w", err)
			}

			return ActorID{
				Foo: parts[0],
				Bar: bar,
			}, nil
		},
	})
}

// NewCustomActorRepository returns an ActorRepository that uses the
// NewCustomActor constructor to create a actors.
func NewCustomActorRepository(repo aggregate.Repository) auth.ActorRepository {
	return repository.Typed(repo, NewCustomActor)
}

func setup(repo aggregate.Repository) {
	actorRepos := auth.NewActorRepositories()

	customRepo := NewCustomActorRepository(repo)

	actorRepos.Add(CustomActorKind, customRepo)

	repo, _ := actorRepos.Repository(CustomActorKind)
	// repo == customRepo
}

func example(l *auth.Lookup, f middleware.Factory) {
	actor := NewCustomActor(uuid.New())
	
	id := ActorID{
		Foo: "foo",
		Bar: 1837,
	}
	// Actors that are not identified by a UUID, must first be identified.
	actor.Identify(id)

	// After being identified, the Lookup maps the formatted id to the actual
	// aggregate id of the actor.
	aggregateID, ok := l.Actor(id.String())
	// ok == true
	// aggregateID == actor.AggregateID()

	// The lookup allows to retrieve the actual aggregate id of a non-UUID-Actor
	// from within the Authorize() middleware.
	mw := f.Authorize(func(auth middleware.Authorizer, r *http.Request) {
		// Extract the formatted custom actor id from the request. 
		var sid string

		// Then lookup the aggregate id (UUID) of that actor 
		id, ok := auth.Lookup(sid)
		// ok == true
		// id == actor.AggregateID()

		auth.Authorize(id)
	})
}
Wildcards

Permissions can be added to actors and roles using wildcards. When a wildcard is provided, it matches all possible values for the given field. Wildcard for string fields is *, wildcard for UUID fields is uuid.Nil.

Example
package example

func example(actor *auth.Actor) {
	// Grant "view" and "update" permission on all aggregates with a specific
	// aggregte id.
	actor.Grant(aggregate.Ref{
		Name: "*",
		ID: uuid.UUID{...},
	}, "view", "update")

	// Grant "view" and "update" permission on all "foo" aggregates.
	actor.Grant(aggregate.Ref{
		Name: "foo",
		ID: uuid.Nil,
	}, "view", "update")

	// Grant permission for all actions on a specific "foo" aggregate.
	actor.Grant(aggregate.Ref{
		Name: "foo",
		ID: uuid.UUID{...},
	}, "*")

	// Grant permission for all actions on all aggregates.
	actor.Grant(aggregate.Ref{
		Name: "*",
		ID: uuid.Nil,
	}, "*")
}
gRPC Server

The authrpc package implements a gRPC server and client that can be used to implement an authorization service. Other services can query the auth service to check permissions of actors and to lookup actor ids.

Protobufs are defined in the github.com/modernice/goes/api/proto package.

package example

import (
	authpb "github.com/modernice/goes/api/proto/gen/auth"
)

func server(perms auth.PermissionRepository) {
	s := grpc.NewServer()
	authpb.RegisterAuthServiceServer(s, authrpc.NewServer(perms))
}

func client(actorID uuid.UUID) {
	conn, err := grpc.Dial(...)
	// handle err

	client := authrpc.NewClient(conn)

	perms, err := client.Permissions(context.TODO(), actorID)
	// handle err

	perms.Allows(...)
	perms.Disallows(...)
}

Documentation

Index

Constants

View Source
const (
	UUIDActor   = "uuid"
	StringActor = "string"
)

Built-in Actor kinds

View Source
const (
	IdentifyActorCmd   = "goes.contrib.auth.actor.identify"
	IdentifyRoleCmd    = "goes.contrib.auth.role.identify"
	GiveRoleToCmd      = "goes.contrib.auth.role.give"
	RemoveRoleFromCmd  = "goes.contrib.auth.role.remove"
	GrantToActorCmd    = "goes.contib.auth.actor.grant"
	RevokeFromActorCmd = "goes.contib.auth.actor.revoke"
	GrantToRoleCmd     = "goes.contib.auth.role.grant"
	RevokeFromRoleCmd  = "goes.contib.auth.role.revoke"
)

Commands

View Source
const (
	ActorIdentified = "goes.contrib.auth.actor.identified"

	RoleIdentified = "goes.contrib.auth.role.identified"
	RoleGiven      = "goes.contrib.auth.role.given"
	RoleRemoved    = "goes.contrib.auth.role.removed"

	// Permission events are used by both the Permission and Role aggregate.
	PermissionGranted = "goes.contrib.auth.permission_granted"
	PermissionRevoked = "goes.contrib.auth.permission_revoked"
)

ActorIdentified is an event constant that represents the identification of an actor within the authentication system. It is used as a key for registering the event data type ActorIdentifiedData in a codec registry.

View Source
const (
	// LookupActor looks up the aggregate id of an actor from a given actor id.
	LookupActor = "actor"

	// LookupRole looks up the aggregate id of a role from a given role name.
	LookupRole = "role"
)
View Source
const ActorAggregate = "goes.contrib.auth.actor"

ActorAggregate is the name of the Actor aggregate.

View Source
const RoleAggregate = "goes.contrib.auth.role"

RoleAggregate is the name of the Role aggregate.

Variables

View Source
var (
	// ErrIDType is returned when trying to identify an actor with an id that
	// has a type other that the configured type.
	ErrIDType = errors.New("invalid id type")

	// ErrMissingActorID is returned when trying to grant or revoke permissions
	// to or from a non-UUID-Actor before the actor has been identified.
	ErrMissingActorID = errors.New("missing actor id")
)
View Source
var (
	// ErrEmptyName is returned when trying to create a role with an empty name.
	ErrEmptyName = errors.New("empty name")

	// ErrMissingRoleName is returned when trying to grant or revoke permissions
	// to or from a role before giving the role a name.
	ErrMissingRoleName = errors.New("missing role name")
)
View Source
var (
	// ErrInvalidRef is returned when providing an invalid aggregate reference
	// to a Grant() or Revoke() call.
	ErrInvalidRef = errors.New("invalid aggregate reference")
)
View Source
var ErrUnknownActorKind = errors.New("unknown actor kind")

ErrUnknownActorKind is returned by ParseKind() if the passed id type is not a builtin actor id type.

Functions

func GiveRoleTo

func GiveRoleTo(roleID uuid.UUID, actors ...uuid.UUID) command.Cmd[[]uuid.UUID]

GiveRoleTo returns the command to give the given role to the given actors.

func GrantToActor

func GrantToActor(actorID uuid.UUID, ref aggregate.Ref, actions ...string) command.Cmd[grantActorPayload]

GrantToActor returns the command to grant the the given actions to the given actor.

func GrantToRole

func GrantToRole(roleID uuid.UUID, ref aggregate.Ref, actions ...string) command.Cmd[grantRolePayload]

GrantToRole returns the command to grant the the given actions to the given role.

func HandleCommands

func HandleCommands(
	ctx context.Context,
	bus command.Bus,
	actorRepos ActorRepositories,
	roles RoleRepository,
	lookup Lookup,
) (<-chan error, error)

HandleCommands handles commands until ctx is canceled.

func IdentifyActor

func IdentifyActor[ID comparable](uid uuid.UUID, id ID) command.Cmd[ID]

IdentifyActor returns the command to specify the id of an actor that is not a UUID-Actor.

func IdentifyRole

func IdentifyRole(id uuid.UUID, name string) command.Cmd[string]

IdentifyRole returns the command to specify the name of the given role.

func ParseKind

func ParseKind(v any) (string, error)

ParseKind is the builtin implementation of ActorRepositories.ParseKind and is used by default if the provided `parseKind` argument that is passed to NewActorRepositories is nil. ParseKind supports parsing of string-Actors and UUID-Actors. If v is neither a string nor a UUID, an error that satisfies errors.Is(err, ErrUnknownActorKind) is returned. To add support for custom actor kinds, pass a custom ParseKind implementation to NewActorRepositories.

func RegisterCommands

func RegisterCommands(r codec.Registerer)

RegisterCommands registers the commands of the auth package into a registry.

func RegisterEvents

func RegisterEvents(r codec.Registerer)

RegisterEvents registers the events of the auth package into a registry.

func RemoveRoleFrom

func RemoveRoleFrom(roleID uuid.UUID, actors ...uuid.UUID) command.Cmd[[]uuid.UUID]

RemoveFoleFrom returns the command to remove the given actors as members from the given role.

func RevokeFromActor

func RevokeFromActor(actorID uuid.UUID, ref aggregate.Ref, actions ...string) command.Cmd[revokeActorPayload]

RevokeFromActor returns the command to revoke the given actions from the given actor.

func RevokeFromRole

func RevokeFromRole(roleID uuid.UUID, ref aggregate.Ref, actions ...string) command.Cmd[revokeRolePayload]

RevokeFromRole returns the command to revoke the the given actions from the given role.

Types

type Actions

type Actions map[aggregate.Ref]map[string]int

Actions is a map that stores granted permissions:

map[AGGREGATE]map[ACTION]GRANT_COUNT

Within the Actor and Role aggregates, GRANT_COUNT is always either 0 or 1.

func (Actions) Equal

func (a Actions) Equal(other Actions) bool

Equal returns whether a and other contain exactly the same values.

type Actor

type Actor struct {
	*aggregate.Base

	Actions
	// contains filtered or unexported fields
}

An Actor represents any kind of user in the system. Actors are granted permissions to perform actions on specific aggregates within an application.

Note that an Actor itself does not provide the full set of permissions that the actor may have. This can be the case if the actor is a member of a role (roles grant permissions to a group of actors). To get the full set of permissions of a specific actor, project the Permissions read-model for that specific actor.

Actors for users that are identified by a UUID can be created using NewUUIDActor.

var userID uuid.UUID
ref := aggregate.Ref{Name: "<aggregate-name>", ID: "<aggregate-id>"}
actor := auth.NewUUIDActor(userID)
actor.Grant(ref, "action-1", "action-2", "...")
actor.Revoke(ref, "action-1", "action-2", "...")

Actors for users that are identified by a string can be created using NewStringActor. Note that the string is not used as the aggregate id (UUID) of the actor. Instead, the string must be passed to the Identify() method of the actor before the actor can be granted or revoked permissions.

ref := aggregate.Ref{Name: "<aggregate-name>", ID: "<aggregate-id>"}
actor := auth.NewStringActor(uuid.New())
actor.Identify("<some-string-id>")
actor.Grant(ref, "action-1", "action-2", "...")
actor.Revoke(ref, "action-1", "action-2", "...")

func NewActor

func NewActor[ID comparable](id uuid.UUID, cfg ActorConfig[ID]) *Actor

NewActor creates a generic actor that uses the provided ActorConfig to format and parse the actor's id (not the aggregate id). NewActor can be used to implement additional actor types besides UUID-Actors and string-Actors. If cfg.ParseID or cfg.FormatID is not provided, NewActor panics.

func NewStringActor

func NewStringActor(id uuid.UUID) *Actor

NewStringActor returns an actor that is identified by a string. A string-Actor may refer to any kind of user that is identified by a simple string. Most commonly, this would be some kind of API key or token.

Example

Imagine an ecommerce app that doesn't force customers to create an account to order products. Customers receive an email with a link that allows them to view and update their order. The links that are sent to the customers include a token that the API uses to authorize the customer's requests.

var orderID uuid.UUID
var token string // generated by the application
actor := auth.NewStringActor(uuid.New())
actor.Identify(token)
actor.Grant("order", orderID, "view", "cancel", ...)

func NewUUIDActor

func NewUUIDActor(id uuid.UUID) *Actor

NewUUIDActor returns the actor that is identified by the provided UUID. A UUID-Actor may refer to any kind of user that is identified by a UUID. Most commonly, this would simply be a user aggregate but it can actually be anything that provides a UUID.

func (*Actor) ActorID

func (a *Actor) ActorID() any

ActorID returns the id of the actor (not the aggregate id).

func (*Actor) ActorKind

func (a *Actor) ActorKind() string

ActorKind returns the kind of the actor. Built-in kinds are "uuid" and "string".

func (*Actor) Allows

func (a *Actor) Allows(action string, ref aggregate.Ref) bool

Allows returns whether the actor is allowed to perform the given action. Allows does not account for the roles the actor is a member of (use the Permissions read-model instead).

func (*Actor) Disallows

func (a *Actor) Disallows(action string, ref aggregate.Ref) bool

Disallows returns whether the actor is allowed to perform the given action. Disallows does not account for the roles the actor is a member of (use the Permissions read-model instead).

func (*Actor) Grant

func (a *Actor) Grant(ref aggregate.Ref, actions ...string) error

Grant grants the actor the permission to perform the given actions on the given aggregate. Grant does not affect the permissions that were granted to the actor through a role.

Wildcards

Grant supports wildcards in the aggregate reference and actions. Pass in a "*" where a string is expected or uuid.Nil where a UUID is expected to match all values.

Example – Grant "view" permission on all aggregates with a specific id:

var id uuid.UUID
actor.Grant(aggregate.Ref{Name: "*", ID: id}, "view")

Example – Grant "view" permission on "foo" aggregates with any id:

actor.Grant(aggregate.Ref{Name: "foo", ID: uuid.Nil}, "view")

Example – Grant "view" permission on all aggregates:

actor.Grant(aggregate.Ref{Name: "*", ID: uuid.Nil}, "view")

Example – Grant all permissions on all aggregates:

actor.Grant(aggregate.Ref{Name: "*", ID: uuid.Nil}, "*")

func (*Actor) Identify

func (a *Actor) Identify(id any) error

Identify sets the provided id as the actor id of the actor. This must be done for any actor that is not a UUID-Actor before the actor can be granted permissions. If the actor is a UUID-Actor, Identify() is a no-op.

func (*Actor) Revoke

func (a *Actor) Revoke(ref aggregate.Ref, actions ...string) error

Revoke revokes the permission to perform the given actions on the given aggregate from the actor. Revoke does not affect the permissions that were granted to the actor through a role.

Wildcards

Revoke supports wildcards in the aggregate reference and actions. Pass in a "*" where a string is expected or uuid.Nil where a UUID is expected to match all values.

Example – Revoke "view" permission on all aggregates with a specific id:

var id uuid.UUID
actor.Revoke(aggregate.Ref{Name: "*", ID: id}, "view")

Example – Revoke "view" permission on "foo" aggregates with any id:

actor.Revoke(aggregate.Ref{Name: "foo", ID: uuid.Nil}, "view")

Example – Revoke "view" permission on all aggregates:

actor.Revoke(aggregate.Ref{Name: "*", ID: uuid.Nil}, "view")

Example – Revoke all permissions on all aggregates:

actor.Revoke(aggregate.Ref{Name: "*", ID: uuid.Nil}, "*")

type ActorConfig

type ActorConfig[ID comparable] struct {
	// Kind is the type of the actor's id. A string-Actor has the "string" kind,
	// a UUID-Actor has the "uuid" kind. The kind is used to get the correct
	// Actor repository from ActorRepositories.
	Kind string

	// ParseID parses the formatted actor id that is returned by FormatID back
	// to the actual ID.
	ParseID func(string) (ID, error)

	// FormatID formats the actor id to a string. The formatted string is used
	// as the event data of the ActorIdentified event.
	FormatID func(ID) string
}

ActorConfig is used by NewActor to configure the actor.

type ActorIdentifiedData

type ActorIdentifiedData string

ActorIdentifiedData is the event data for ActorIdentified.

func (ActorIdentifiedData) ProvideLookup

func (data ActorIdentifiedData) ProvideLookup(p lookup.Provider)

ProvideLookup implements lookup.Event.

type ActorRepositories

type ActorRepositories interface {
	// ParseKind parses actor kinds from ids.
	ParseKind(any) (string, error)

	// Repository returns the repository for the given actor kind.
	Repository(kind string) (ActorRepository, error)
}

ActorRepositories provides Actor repositories for different kinds of actors.

type ActorRepository

type ActorRepository = aggregate.TypedRepository[*Actor]

ActorRepository is the repository for Actors.

func NewStringActorRepository

func NewStringActorRepository(repo aggregate.Repository) ActorRepository

NewStringActorRepository returns the repository for string-Actors.

func NewUUIDActorRepository

func NewUUIDActorRepository(repo aggregate.Repository) ActorRepository

NewUUIDActorRepository returns the repository for UUID-Actors.

type ActorRepositoryRegistry

type ActorRepositoryRegistry struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

ActorRepositoryRegistry is a registry for Actor repositories of different kinds.

func NewActorRepositories

func NewActorRepositories(repo aggregate.Repository, parseKind func(any) (string, error)) *ActorRepositoryRegistry

NewActorRepositories returns an ActorRepositoryRegistry that provides repositories for builtin actor kinds (UUIDActor and StringActor).

func NewEmptyActorRepositories

func NewEmptyActorRepositories(parseKind func(any) (string, error)) *ActorRepositoryRegistry

NewEmptyActorRepositories returns a fresh ActorRepositoryRegistry. If provided, the parseKind function is used to parse actor kinds from formatted actor ids.

func (*ActorRepositoryRegistry) Add

func (repos *ActorRepositoryRegistry) Add(kind string, repo ActorRepository)

Add adds the ActorRepository for the given actor kind to the registry.

func (*ActorRepositoryRegistry) ParseKind

func (repos *ActorRepositoryRegistry) ParseKind(v any) (string, error)

ParseKind implements ActorRepositories.ParseKind.

func (*ActorRepositoryRegistry) Repository

func (repos *ActorRepositoryRegistry) Repository(kind string) (ActorRepository, error)

Repository returns the actor repository for the given actor kind.

type CommandClient

type CommandClient interface {
	// GrantToActor grants the given actor the permission to perform the given actions.
	GrantToActor(context.Context, uuid.UUID, aggregate.Ref, ...string) error

	// GrantToRole grants the given role the permission to perform the given actions.
	GrantToRole(context.Context, uuid.UUID, aggregate.Ref, ...string) error

	// RevokeFromActor revokes from the given actor the permission to perform the given actions.
	RevokeFromActor(context.Context, uuid.UUID, aggregate.Ref, ...string) error

	// RevokeFromRole revokes from the given role the permission to perform the given actions.
	RevokeFromRole(context.Context, uuid.UUID, aggregate.Ref, ...string) error
}

CommandClient defines the command client for the authorization module. It exposes the commands to grant and revoke permissions as an interface. Each of these commands is also available as a "standalone" command:

  • auth.CommandClient.GrantToActor() -> auth.GrantToActor()
  • auth.CommandClient.GrantToRole() -> auth.GrantToRole()
  • auth.CommandClient.RevokeFromActor() -> auth.RevokeFromActor()
  • auth.CommandClient.RevokeFromRole() -> auth.RevokeFromRole()

Use the CommandBusClient() constructor to create a CommandClient from an underlying command bus. Alternatively, use the RepositoryCommandClient() to create a CommandClient from actor and role repositories, or use authrpc.NewClient() to create a gRPC CommandClient.

func CommandBusClient

func CommandBusClient(bus command.Bus, opts ...command.DispatchOption) CommandClient

CommandBusClient returns a CommandClient that executes commands by dispatching them via the provided command bus. The provided dispatch options are applied to all dispatched commands.

func RepositoryCommandClient

func RepositoryCommandClient(actors ActorRepositories, roles RoleRepository) CommandClient

RepositoryCommandClient returns a CommandClient that executes commands directly on the Actor and Role aggregates within the provided repositories.

type Granter

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

Granter subscribes to user-provided events to trigger permission changes. Granter can be used to automatically grant or revoke permissions to and from actors and roles when a specified event is published over the underlying event bus.

Granter applies permission changes retrospectively for past events on startup to ensure that new permissions are applied to existing actors and roles.

func NewGranter

func NewGranter(
	events []string,
	client CommandClient,
	lookup Lookup,
	bus event.Bus,
	store event.Store,
	opts ...GranterOption,
) *Granter

NewGranter returns a new permission granter background task.

var events []string
var actors auth.ActorRepositories
var roles auth.RoleRepository
var lookup *auth.Lookup
var bus event.Bus

g := auth.NewGranter(events, actors, roles, lookup, bus)
errs, err := g.Run(context.TODO())

func (*Granter) GrantOn

func (g *Granter) GrantOn(handler func(TargetedGranter, event.Event) error, eventNames ...string)

GrantOn registers a manual handler for the given event. See the package-level GrantOn function for more details and type parameterized handler registration.

func (*Granter) Ready

func (g *Granter) Ready() <-chan struct{}

Ready returns a channel that blocks until the granter applied a projection job for the first time. Waiting for <-g.Ready() ensures that the permissions of all actors are up-to-date. Ready should not be called before g.Run() is called, otherwise it will return a nil-channel that blocks forever. Ready must not be called before g.Run() returns, to avoid race conditions.

func (*Granter) Run

func (g *Granter) Run(ctx context.Context) (<-chan error, error)

Run runs the permission granter until ctx is canceled.

type GranterOption

type GranterOption func(*Granter)

GranterOption is a permission granter option.

func GrantOn

func GrantOn[Data any](handler func(TargetedGranter, event.Of[Data]) error, eventNames ...string) GranterOption

GrantOn returns a GranterOption that registers a manual handler for the given events. Instead of checking if the event data implements PermissionGranterEvent, the handler is called directly with the same TargetedGranter that would be passed to a PermissionGranterEvent.

Event names that are registered using the GrantOn() option do not have to be provided to NewGranter(); they are automatically added to the list of events that are subscribed to:

// <nil> events provided, but "foo", "bar", and "baz" events are subscribed to
g := auth.NewGranter(nil, ..., auth.GrantOn(..., "foo", "bar", "baz"))

Alternatively, if you already have an exisiting *Granter g, call g.GrantOn() to register additional handlers.

type Lookup

type Lookup interface {
	// Actor returns the aggregate id of the actor with the given string-formatted actor id.
	Actor(context.Context, string) (uuid.UUID, bool)

	// Role returns the aggregate id of the role with the given name.
	Role(context.Context, string) (uuid.UUID, bool)
}

Lookup provides lookups of actor ids and role ids.

func ClientLookup

func ClientLookup(client QueryClient) Lookup

ClientLookup returns a Lookup that uses the provided client to do the lookups.

type LookupTable

type LookupTable struct {
	*lookup.Lookup
}

LookupTable provides lookups from actor ids to aggregate ids of those actors.

func NewLookup

func NewLookup(store event.Store, bus event.Bus, opts ...lookup.Option) *LookupTable

NewLookup returns a new lookup for aggregate ids of actors.

func (*LookupTable) Actor

func (l *LookupTable) Actor(ctx context.Context, id string) (uuid.UUID, bool)

Actor returns the aggregate id of the actor with the given formatted actor id.

func (*LookupTable) Role

func (l *LookupTable) Role(ctx context.Context, name string) (uuid.UUID, bool)

Role returns the aggregate id of the role with the given name.

type PermissionFetcher

type PermissionFetcher interface {
	// Fetch fetches the permissions of the given actor.
	Fetch(context.Context, uuid.UUID) (PermissionsDTO, error)
}

PermissionFetcher fetches permissions of actors.

type PermissionFetcherFunc

type PermissionFetcherFunc func(context.Context, uuid.UUID) (PermissionsDTO, error)

PermissionFetcherFunc allows a function to be used as a PermissionFetcher.

func ClientPermissionFetcher

func ClientPermissionFetcher(client QueryClient) PermissionFetcherFunc

ClientPermissionFetcher returns a PermissionFetcher that fetches permissions using the provided client. ClientPermissionFetcher simply returns the client.Permissions method, which is a PermissionFetcherFunc.

func RepositoryPermissionFetcher

func RepositoryPermissionFetcher(repo PermissionRepository) PermissionFetcherFunc

RepositoryPermissionFetcher returns a PermissionFetcher that fetches permissions using the provided PermissionRepository.

func (PermissionFetcherFunc) Fetch

func (fetch PermissionFetcherFunc) Fetch(ctx context.Context, actorID uuid.UUID) (PermissionsDTO, error)

Fetch retrieves permissions for a given actor using the PermissionRepository provided to the RepositoryPermissionFetcher. It returns a PermissionsDTO and an error if any.

type PermissionGrantedData

type PermissionGrantedData struct {
	Aggregate aggregate.Ref
	Actions   []string
}

PermissionGrantedData is the event data for PermissionGranted.

type PermissionGranterEvent

type PermissionGranterEvent interface {
	// GrantPermissions is called by *Granter when the event that implements
	// this interface is published.
	GrantPermissions(TargetedGranter) error
}

PermissionGranterEvent must be implemented by event data to be used within a *Granter. Event data that implements this interface can grant and revoke permissions to and from actors and roles. When such an event is published over the event bus, the running *Granter calls the event data's GrantPermissions() method with a TargetedGranter. The aggregate of the event is used as the permission target.

type PermissionProjector

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

PermissionProjector continuously projects the Permissions read-model for all actors.

func NewPermissionProjector

func NewPermissionProjector(
	perms PermissionRepository,
	roles RoleRepository,
	bus event.Bus,
	store event.Store,
	opts ...schedule.ContinuousOption,
) *PermissionProjector

NewPermissionProjector returns a new permission projector.

func (*PermissionProjector) Run

func (proj *PermissionProjector) Run(ctx context.Context) (<-chan error, error)

Run projects permissions until ctx is canceled.

type PermissionRepository

type PermissionRepository = model.Repository[*Permissions, uuid.UUID]

PermissionRepository is the repository for the permission read-models.

func InMemoryPermissionRepository

func InMemoryPermissionRepository() PermissionRepository

InMemoryPerissionRepository returns an in-memory repository for the permission read-models.

func MongoPermissionRepository

func MongoPermissionRepository(ctx context.Context, col *gomongo.Collection) (PermissionRepository, error)

MongoPermissionRepository returns a MongoDB repository for the permission read-models. An index for the "actorId" field is automatically created if it does not exist yet.

TODO(bounoable): This should move somewhere else to not pollute this package with backend implementations.

type PermissionRevokedData

type PermissionRevokedData struct {
	Aggregate aggregate.Ref
	Actions   []string
}

PermissionRevokedData is the event data for PermissionRevoked.

type Permissions

type Permissions struct {
	*projection.Base
	*projection.Progressor
	PermissionsDTO
	// contains filtered or unexported fields
}

Permissions is the read-model for the permissions of a specific actor. Permissions uses the actor and role events to project the permissions of a specific actor. An actor is allowed to perform a given action if either the actor itself was granted the permission, or if the actor is a member of a role that was granted the permission.

In order to fully remove a permission of an actor, the permission needs to be revoked from the Actor itself and also from all roles the actor is a member of (or the actor must be removed from these roles).

For example, if an actor is a member of an "admin" role, and the following permissions are granted and revoked in the following order:

  1. Actor is granted "view" permission on a "foo" aggregate.
  2. Role is granted "view" permission on the same "foo" aggregate.
  3. Role is revoked "view" permission on the aggregate.

Then the actor is still allowed to perform the "view" action on the aggregate.

Another example:

  1. Role is granted "view" permission on a "foo" aggregate.
  2. Actor is granted "view" permission on the same aggregate.
  3. Actor is revoked "view" permission on the aggregate.

Then the actor is also allowed to perform the "view" action on the aggregate because the role still grants the permission its members.

func PermissionsOf

func PermissionsOf(actorID uuid.UUID) *Permissions

PermissionsOf returns the permissions read-model of the given actor. The returned projection has an empty state. A *Projector can be used to continuously project the permission read-models for all actors. Use a PermissionRepository to fetch the projected permissions of an actor:

var repo auth.PermissionRepository
var actorID uuid.UUID
perms, err := repo.Fetch(context.TODO(), actorID)
// handle err
allowed := perms.Allows("<action>", aggregate.Ref{Name: "...", ID: uuid.UUID{...}})
disallowed := perms.Disallows("<action>", aggregate.Ref{Name: "...", ID: uuid.UUID{...}})

type PermissionsDTO

type PermissionsDTO struct {
	ActorID uuid.UUID   `json:"actorId"`
	Roles   []uuid.UUID `json:"roles"`
	OfActor Actions     `json:"ofActor"`
	OfRoles Actions     `json:"ofRoles"`
}

PermissionsDTO is the DTO of Permissions.

func (PermissionsDTO) ActorAllows

func (perms PermissionsDTO) ActorAllows(action string, ref aggregate.Ref) bool

ActorAllows returns whether the actor is allowed to perform the given action on the given aggregate, ignoring permissions of any roles the actor is member of.

func (PermissionsDTO) Allows

func (perms PermissionsDTO) Allows(action string, ref aggregate.Ref) bool

Allows returns whether the actor is allowed to perform the given action on the given aggregate. An actor is allowed to perform a given action if either the actor itself was granted the permission, or if the actor is a member of a role that was granted the permission.

Read the documentation of Permissions for more details.

func (PermissionsDTO) Disallows

func (perms PermissionsDTO) Disallows(action string, ref aggregate.Ref) bool

Disallows returns whether the actor is disallows to perform the given action on the given aggregate. Disallows simply returns !perms.Allows(action, ref).

Read the documentation of Permissions for more details.

func (PermissionsDTO) Equal

func (perms PermissionsDTO) Equal(other PermissionsDTO) bool

Equal returns whether perms and other contain exactly the same values.

func (PermissionsDTO) ModelID

func (perms PermissionsDTO) ModelID() uuid.UUID

ModelID returns the aggregate id of the actor. ModelID implements goes/persistence/model.Model.

func (PermissionsDTO) RoleAllows

func (perms PermissionsDTO) RoleAllows(action string, ref aggregate.Ref) bool

RoleAllows returns whether the actor is allowed to perform the given action on the given aggregate, using only the permissions of the roles the actor is member of.

type QueryClient

type QueryClient interface {
	// Permissions returns the permission read-model of the given actor.
	Permissions(ctx context.Context, actorID uuid.UUID) (PermissionsDTO, error)

	// Allows returns whether the given actor has the permission to perform the
	// given action on the given aggregate.
	Allows(ctx context.Context, actorID uuid.UUID, ref aggregate.Ref, action string) (bool, error)

	// LookupActor looks up the aggregate id of the actor with the given
	// formatted actor id.
	LookupActor(ctx context.Context, sid string) (uuid.UUID, error)

	// LookupRole looks up the agggregate id of the role with the given name.
	LookupRole(ctx context.Context, name string) (uuid.UUID, error)
}

QueryClient defines the query client for the authorization module.

QueryClient is implemented by goes/contrib/auth/authrpc.Client.

type Role

type Role struct {
	*aggregate.Base

	Actions
	// contains filtered or unexported fields
}

Role represents a named authorization role. Like actors, roles can be granted permissions to perform actions on specific aggregates. Actors can be added to and removed from roles. Actors that are members of a role inherit the role's permissions. A role must be given a name before it can be granted permissions.

Example: "admin" role

role := auth.NewRole(uuid.New())
role.Identify("admin")
role.Grant(aggregate.Ref{Name: "foo", ID: uuid.UUID{...}}, "read", "write")

func NewRole

func NewRole(id uuid.UUID) *Role

NewRole returns the role with the given id.

func (*Role) Add

func (r *Role) Add(actors ...uuid.UUID) error

Add adds the given actors as members to the role.

func (*Role) Allows

func (r *Role) Allows(action string, ref aggregate.Ref) bool

Allows returns whether the role has the permission to perform the given action.

func (*Role) Disallows

func (r *Role) Disallows(action string, ref aggregate.Ref) bool

Disallows returns whether the role does not have the permission to perform the given action.

func (*Role) Grant

func (r *Role) Grant(ref aggregate.Ref, actions ...string) error

Grant grants the role the permission to perform the given actions on the given aggregate.

Wildcards

Grant supports wildcards in the aggregate reference and actions. Pass in a "*" where a string is expected or uuid.Nil where a UUID is expected to match all values.

Example – Grant "view" permission on all aggregates with a specific id:

var id uuid.UUID
role.Grant(aggregate.Ref{Name: "*", ID: id}, "view")

Example – Grant "view" permission on "foo" aggregates with any id:

role.Grant(aggregate.Ref{Name: "foo", ID: uuid.Nil}, "view")

Example – Grant "view" permission on all aggregates:

role.Grant(aggregate.Ref{Name: "*", ID: uuid.Nil}, "view")

Example – Grant all permissions on all aggregates:

role.Grant(aggregate.Ref{Name: "*", ID: uuid.Nil}, "*")

func (*Role) Identify

func (r *Role) Identify(name string) error

Identify identifies the role with the given name, which must must not be empty. Identify must be called before r.Grant() or r.Revoke() is called; otherwise these methods will return an error that satisfies errors.Is(err, ErrMissingRoleName).

func (*Role) IsMember

func (r *Role) IsMember(actorID uuid.UUID) bool

IsMember returns whether the given actor is a member of this role.

func (*Role) Name

func (r *Role) Name() string

Name returns the name of the role.

func (*Role) Remove

func (r *Role) Remove(actors ...uuid.UUID) error

Remove removes the given actors as members from the role.

func (*Role) Revoke

func (r *Role) Revoke(ref aggregate.Ref, actions ...string) error

Revoke revokes the role's permission to perform the given actions on the given aggregate.

Wildcards

Revoke supports wildcards in the aggregate reference and actions. Pass in a "*" where a string is expected or uuid.Nil where a UUID is expected to match all values.

Example – Revoke "view" permission on all aggregates with a specific id:

var id uuid.UUID
role.Revoke(aggregate.Ref{Name: "*", ID: id}, "view")

Example – Revoke "view" permission on "foo" aggregates with any id:

role.Revoke(aggregate.Ref{Name: "foo", ID: uuid.Nil}, "view")

Example – Revoke "view" permission on all aggregates:

role.Revoke(aggregate.Ref{Name: "*", ID: uuid.Nil}, "view")

Example – Revoke all permissions on all aggregates:

role.Revoke(aggregate.Ref{Name: "*", ID: uuid.Nil}, "*")

type RoleIdentifiedData

type RoleIdentifiedData string

RoleIdentifiedData is the event data for RoleIdentified.

func (RoleIdentifiedData) ProvideLookup

func (data RoleIdentifiedData) ProvideLookup(p lookup.Provider)

ProvideLookup implements lookup.Event.

type RoleRepository

type RoleRepository = aggregate.TypedRepository[*Role]

RoleRepository is the repository for Roles.

func NewRoleRepository

func NewRoleRepository(repo aggregate.Repository) RoleRepository

NewRoleRepository returns the repository for Roles.

type TargetedGranter

type TargetedGranter interface {
	// Context is the context of the underlying *Granter.
	Context() context.Context

	// Target returns the permission target for the granted and revoked permissions.
	Target() aggregate.Ref

	// Lookup returns the lookup that can be used to resolve actor and role ids.
	Lookup() Lookup

	// GrantToActor grants the given actor the permission to perform the given
	// actions on the aggregate referenced by Target().
	GrantToActor(ctx context.Context, actorID uuid.UUID, actions ...string) error

	// RevokeFromActor revokes from the given actor the permission to perform
	// the given actions on the aggregate referenced by Target().
	RevokeFromActor(ctx context.Context, actorID uuid.UUID, actions ...string) error

	// GrantToRole grants the given role the permission to perform the given
	// actions on the aggregate referenced by Target().
	GrantToRole(ctx context.Context, roleID uuid.UUID, actions ...string) error

	// RevokeFromRole revokes from the given role the permission to perform
	// the given actions on the aggregate referenced by Target().
	RevokeFromRole(ctx context.Context, roleID uuid.UUID, actions ...string) error
}

TargetedGranter provides grant and revoke methods for the actions on a specific aggregate. The provided GrantToXXX() and RevokeFromXXX() methods grant the given actor or role permission to perform the given actions on the aggregate referenced by Target().

TargetedGranter is passed to PermissionGranterEvent implementations by a *Granter when an event with such data is published over the *Granter's underlying event bus.

Directories

Path Synopsis
http

Jump to

Keyboard shortcuts

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