auth

package module
v0.0.0-...-e0b7b9b Latest Latest
Warning

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

Go to latest
Published: Feb 11, 2021 License: AGPL-3.0 Imports: 12 Imported by: 0

README

auth

A package to provide user-authentication as well as authorization (at some point in time).

Authentication

var (
    err  error
    user User
)

defer session.Cancel() // Will write internal `err` as Response, in case of error.

if user, err := session.Auth(); err != nil {
    log.Printf(err.Error())
    return
}

...

Authorization

A simple, familiar system inspired by the likes of Google Cloud Platform, BitBucket, and Atlassian Confluence.

Philosophy

auth aims to simply provide a way to persist various roles a user may be assigned for resources. It doesn't implicitly infer any hierarchy among these roles or resources. For example, whether users with the role of Owners are allowed to do everything those with the role of Editors can, is left up to your business-logic. The idea is to keep our persistence of authorization as light as possible, so as to avoid the any complex migrations to them in case the rules change on us.

Resources are themselves more often than not hierarchical in nature - i.e. an Account can contain multiple Campaigns, and therefore an Editor of Account is also an Editor of it's underlying Campaigns. However, we steer clear of any such rule-definitions in our framework. This allows the developer to build both, implicitly whitelisting, as well as explicitly blacklisting systems as she may deem fit for her use-case.

This impedes us from providing certain auto-magic out-of-the-box, like disallowing a end-user from deleting herself from an Owners group. Rather, it transfers this responsibility to the developer, who may choose to allow it (creating a sophisticated system for ownership transfers), or disallow it.

Lastly, we make a rather bold deviation from most authorization-frameworks by not even persisting what actions a Role may allow a User to perform upon a Resource - such as Create, Read, Update, Delete or a combination of the aforementioned. This is largely because it is often unnatural for actions to be labeled so. Take for instance an email-sending system - we can easily see how an eXecute label would be required for creating a sophisticated system. For other Resources within the same system, this label would make little sense. As fewer things are scarcer that discipline among software-developers, we steer clear of a situation where system-wide changes would require us to relabel all the persisted actions, by not persisting actions to begin with. In sophisticated systems, forcing CRUD labels onto Roles create more problems than they would solve.

Ubiquitous Language
  • A Resource represents an Entity in the business-layer.
  • A Role represents a logical set of allowed actions on a Resource.
  • A Group is a set of Users for a Role.
  • A User is allowed to perform an action if present in one or more Groups for the corresponding Role(s).
Usage

Inside Campaign:

func (campaign Campaign) Resource() (kind auth.ResourceKind, id auth.ResourceID) {
    return ResourceKind, auth.ResourceID(campaign.ID)
}

func (campaign Campaign) NewOwnerRole() (ownerRole auth.Role) {
    return NewRole(OwnerRole, campaign.Resource())
}

func (campaign Campaign) OwnerRoles() (ownerRoles auth.Roles) {
    ownerRoles = append(
        ownerRoles,
        campaign.NewOwnerRole(),
        campaign.User.OwnerRoles()..., // Refers to the parent account
    )

    return
}

Now, while creating a new Campaign:

// Add the creator of a Campaign to it's 'Owner' group.
auth.Groups.Add(ctx, user, campaign.NewOwnerRole())

Sometime later, in our Services...

// Check
if ok, err = auth.Groups.Belongs(ctx, user, campaign.OwnerRoles()); err != nil {
    return
} else if !ok {
    err = fmt.Errorf("only owner(s) can perform this action")
    return
}

...

Documentation

Index

Constants

View Source
const (
	// IgnoreUnverified and pass Auth.
	IgnoreUnverified = Option(1)
)
View Source
const (
	// UserKey for storing auth.Auth with context.WithValue(...).
	UserKey = Key("user")
)

Variables

View Source
var (
	// ErrMissingUserCredentials when auth information isn't present in message.
	ErrMissingUserCredentials = fmt.Errorf("missing user credentials")

	// ErrInvalidUserCredentials when ID, Secret doesn't match that in the database. We
	// use this generic error by design so as to thwart malicious requests intended to
	// list user IDs by brute-force.
	ErrInvalidUserCredentials = fmt.Errorf("invalid user credentials")

	// ErrUserNotVerified when user hasn't verified email ID, phone, etc.
	ErrUserNotVerified = fmt.Errorf("user not verified")
)
View Source
var AWSCognitoModule = fx.Options(
	fx.Provide(
		NewAWSCognitoRBAC,
	),
	fx.Invoke(
		WithRBAC,
	),
)

AWSCognitoModule is an fx.Options that sets up our AWS Cognito RBAC.

View Source
var (
	// ErrUnauthorizedUser when User lacks necessary permissions to make a request.
	ErrUnauthorizedUser = fmt.Errorf("user not authorized to make that request")
)

Some handy errors you can use to send out of packages that depend on auth.

View Source
var GRPCUnaryInterceptor grpc.UnaryServerInterceptor = func(
	ctx context.Context,
	req interface{},
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (resp interface{}, err error) {
	if custom, ok := info.Server.(GRPCUnaryInterceptorOverride); ok {
		if ctx, err = custom.Auth(ctx, info.FullMethod); err != nil {
			return
		}
	} else if ctx, err = GRPCAuthFunc(ctx); err != nil {
		return
	}

	resp, err = handler(ctx, req)
	return
}

GRPCUnaryInterceptor to Auth incoming requests.

Module is an fx.Options that includes provider (constructors) and invoke (register) functions of the package

View Source
var Owner = Role{
	Name:     RoleName("owner"),
	Resource: PlatformResource,
}

Owner is the role that has access to all resources

View Source
var PlatformResource = resourceImpl{
	// contains filtered or unexported fields
}

PlatformResource is the top level Resource that contains all other Resources.

Functions

func ConfigMaster

func ConfigMaster(id string, secret string)

ConfigMaster sets the details for the user that has access to everything

func GRPCAuthFunc

func GRPCAuthFunc(ctx context.Context) (authCtx context.Context, err error)

GRPCAuthFunc matches grpc_auth.AuthFunc, in case you want to use github.com/grpc-ecosystem/go-grpc-middleware

func HTTPHandler

func HTTPHandler(handler http.HandlerFunc) (authHandler http.HandlerFunc)

HTTPHandler to chain with our HandlerFuncs, performing Auth before invoking them.

func Hash

func Hash(password string, salts ...string) string

Hash is a convenience function that appends the password along with any other passed salts to return an SHA1 hash.

func IsMaster

func IsMaster(id, secret string) (ok bool)

IsMaster is a convenience-method to check whether the User is Master or not.

func WithGroupRepository

func WithGroupRepository(r GroupRepository)

WithGroupRepository configures the GroupRepository implementation that `auth` will refer.

func WithRBAC

func WithRBAC(r RBAC)

WithRBAC configures the RBAC implementation that `auth` will refer.

func WithRepository

func WithRepository(r Repository)

WithRepository configures the User Repository implementation that `auth` will refer.

Types

type AWSCognitoRBAC

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

AWSCognitoRBAC implements the RBAC interface for AWS Cognito.

func (*AWSCognitoRBAC) Authenticate

func (repo *AWSCognitoRBAC) Authenticate(
	ctx context.Context, username, accessToken string,
) (user User, ok bool, err error)

Authenticate implements Repository#Authenticate for AWS Cognito.

type GRPCSession

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

GRPCSession is an implementation of Session for gRPC, and checks for User and Secret in the Auth protobuf-generated message-struct.

func NewGRPCSession

func NewGRPCSession(ctx context.Context) (session GRPCSession)

NewGRPCSession is a constructor for GRPCSession. It mutates the Context pointed to, by inserting the User into it's Values when Auth() is called.

func (GRPCSession) Auth

func (session GRPCSession) Auth() (ctx context.Context, err error)

Auth checks whether the User and Secret are valid credentials per our Repository.

func (GRPCSession) Cancel

func (session GRPCSession) Cancel()

Cancel the underlying gRPC Context.

type GRPCUnaryInterceptorOverride

type GRPCUnaryInterceptorOverride interface {
	Auth(ctx context.Context, fullMethodName string) (
		authCtx context.Context, err error,
	)
}

GRPCUnaryInterceptorOverride is an interface a gRPC Server can implement to override the global server GRPCUnaryInterceptor installation and implement custom authentication.

type Group

type Group struct {
	Role  Role
	Users []string
}

Group of Users with a common Role. Groups are how we interface with our persistence API to assign Roles to Users or list all Users with a given Role. In a Google Cloud Platform-like implementation, we would show the multiple groups for a given Resource and allow our end-user to configure them. In a BitBucket-like implementation, we'd mix up all the Groups for a given Resource while displaying them, with the relevant Role selected next to a Given user. This would allow us to change the Role assigned to a given User with a single click.

type GroupMySQLRepository

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

GroupMySQLRepository implements GroupRepository in MySQL.

func (*GroupMySQLRepository) Add

func (repo *GroupMySQLRepository) Add(
	ctx context.Context, user User, role Role,
) (err error)

Add a User to a Group for the given Role, creating a Group if it doesn't exist. Add is an idempotent action and does nothing silently if the User already has the given Role.

func (*GroupMySQLRepository) Delete

func (repo *GroupMySQLRepository) Delete(
	ctx context.Context, user User, role Role,
) (err error)

Delete a Role for a User.

func (*GroupMySQLRepository) Find

func (repo *GroupMySQLRepository) Find(ctx context.Context, role Role) (
	group Group, err error,
)

Find a Group of Users that are assigned a given Role. This allows us to visually list them and allow for the end-user to reconfigure our Groups.

func (*GroupMySQLRepository) Free

func (repo *GroupMySQLRepository) Free(ctx context.Context, resource Resource) (
	err error,
)

Free deletes all Groups attached to the given Resource. It is intended to be called at the end of the Resource's lifecycle.

func (*GroupMySQLRepository) IsUserInAny

func (repo *GroupMySQLRepository) IsUserInAny(ctx context.Context, user User, roles Roles) (
	ok bool, err error,
)

IsUserInAny checks whether the given User has one or more of the given Roles. It's results can only be reliably consumed when `err` is `nil`.

func (*GroupMySQLRepository) Resources

func (repo *GroupMySQLRepository) Resources(
	ctx context.Context, kind ResourceKind, user User,
) (roles Roles, err error)

Resources lists all of the Resources of a given ResourceKind that a User has access to via any Role assigned to her.

type GroupRepository

type GroupRepository struct {
	GroupRepositoryImpl
}

GroupRepository persists our Groups using an underlying GroupRepositoryImpl.

var Groups GroupRepository

Groups exposes our internal GroupRepository as a public API for our business layer.

func NewGroupMySQLRepositoryImpl

func NewGroupMySQLRepositoryImpl(db *sql.DB) (repo GroupRepository, err error)

NewGroupMySQLRepositoryImpl is a constructor for GroupMySQLRepository.

func (GroupRepository) IsInAny

func (repo GroupRepository) IsInAny(ctx context.Context, roles Roles) (
	ok bool, err error,
)

IsInAny is a convenience-method that calls `IsUserInAny` with the User in the current Context.

type GroupRepositoryImpl

type GroupRepositoryImpl interface {
	Add(ctx context.Context, user User, role Role) (err error)
	Delete(ctx context.Context, user User, role Role) (err error)
	Find(ctx context.Context, role Role) (group Group, err error)
	Free(ctx context.Context, resource Resource) (err error)
	IsUserInAny(ctx context.Context, user User, roles Roles) (ok bool, err error)
	Resources(ctx context.Context, kind ResourceKind, user User) (
		roles Roles, err error,
	)
}

GroupRepositoryImpl defines an interface with which we can persist our Groups.

type HTTPSession

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

HTTPSession implements Session over HTTP headers.

func NewHTTPSession

func NewHTTPSession(rw http.ResponseWriter, req *http.Request) (session *HTTPSession)

NewHTTPSession is a constructor for HTTPSession.

func (*HTTPSession) Auth

func (session *HTTPSession) Auth() (ctx context.Context, err error)

Auth authenticates a Session based.

func (*HTTPSession) Cancel

func (session *HTTPSession) Cancel()

Cancel checks if an error has occurred thus far and writes it to the HTTP response.

type Key

type Key string

Key is a non-simple type for keys in the context.Context operations by Sessions.

type Option

type Option int

Option to configure different behaviour for Session#Auth.

type Query

type Query struct {
	UserEmail string
	Kind      ResourceKind
	RoleName  RoleName
}

Query returns a fragment that can be used to authenticate resource access.

func NewQuery

func NewQuery(
	userEmail string, kind ResourceKind, roleName RoleName,
) (query Query)

NewQuery is a constructor for Query.

func (Query) AsSQL

func (query Query) AsSQL() (sql string, params []interface{}, err error)

AsSQL return an SQL fragment that can be used to authenticate resources.

type RBAC

type RBAC interface {
	Authenticate(
		ctx context.Context, username, password string,
	) (user User, ok bool, err error)
}

RBAC is an interface that any RBAC provider must implement.

func NewAWSCognitoRBAC

func NewAWSCognitoRBAC(
	cfg aws.Config,
) (rbac RBAC, err error)

NewAWSCognitoRBAC is the provider for an AWS Cognito-backed Repository.

type RBACUser

type RBACUser struct {
}

RBACUser provides stubs for User#Secret and User#IsVerified as RBAC-implementations often do not maintain these values as part of their business logic, but instead delegate it to their RBAC system.

func (RBACUser) GetIsVerified

func (user RBACUser) GetIsVerified() (ok bool)

GetIsVerified stubs User#GetIsVerified.

func (RBACUser) GetSecret

func (user RBACUser) GetSecret() (secret string)

GetSecret stubs User#Secret.

type Repository

type Repository interface {
	FindAuthUser(ctx context.Context, id string) (User, bool, error)
}

Repository is the interface for your application's repository to implement.

type Resource

type Resource interface {
	Identifier() (id ResourceID)
	Kind() (kind ResourceKind)
}

Resource is an interface that must be implemented by entities.

type ResourceID

type ResourceID string

ResourceID to identify Resources.

type ResourceKind

type ResourceKind string

ResourceKind to get the type of a Resource.

type Role

type Role struct {
	Name     RoleName
	Resource Resource
}

Role represents a realm of allowed actions that it allows upon a Resource. These actions themselves are 'mapped' to the Role in our business-logic, and aren't persisted. This allows us to iterate on our set of Roles without any 'migrations' upon our persisted data.

func NewRole

func NewRole(name RoleName, resource Resource) (role Role)

NewRole is a convenience-constructor for Role.

type RoleName

type RoleName string

RoleName is a string-based key to ensure our Roles are named uniquely through our codebase.

type Roles

type Roles []Role

Roles is a type-alias for []Role.

func RolesFor

func RolesFor(name RoleName, resources ...Resource) (roles Roles)

RolesFor is a convenience-constructor for constructing an array of Roles with the same name but for different Resources. This is handy when a Role 'propagates' hierarchically through a set of Resources. For example, if an "Editor" Role for an Account implies an "Editor" Role for all it's constituent entities, we can use auth.Roles to check if the User has the "Editor" Role for the Account, or the child entity being accessed. The `auth` package doesn't assume any such hierarchical relationships and may be considered more as a handy labelling tool rather than housing for actual authorization business-logic. It merely appends to a set of Groups and allows us to check on the presence of a User in an array of Roles.

func (Roles) IDs

func (roles Roles) IDs() (ids []string)

IDs gives us all ResourceIDs in our Roles as []string. It is handy for looking up lists of our entities that the User has some kind of access to.

type Session

type Session interface {
	Auth() (context.Context, error)
	Cancel()
}

Session for an authenticated User.

type User

type User interface {
	GetID() string
	GetSecret() string
	GetIsVerified() bool
}

User is a generic type that requires your application-level Users to implement certain

func FromContext

func FromContext(ctx context.Context) (user User, err error)

FromContext creates a User from a context.Context.

Jump to

Keyboard shortcuts

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