okay

package module
v0.0.0-...-0c60c5b Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2017 License: Apache-2.0 Imports: 4 Imported by: 5

README

okay: abstract cross-package auth[nz]

okay is a package that allows consumers to pass authentication and authorization checks across package boundaries.

Consumers should start with the base OK, which denies everyone to everything, and add permissions as required with the Allow, Validate, and Verify functions.

Overview

Often there is some resource to which we want to restrict access; databases (or columns, or rows), file systems, URL endpoints, or even specific functions or methods.

Say we have a file system type:

package fs

import (
	"io"
	"os"
	"path/filepath"
)

type FileSystem struct {
	Root string
}

func (fs FileSystem) Open(path string) (io.ReadCloser, error) {
	return os.Open(filepath.Join(fs.Root, path))
}

This could be used to e.g. serve the file via HTTP, but it provides no access controls. We could add this by modifying Open to take a Context which contains the appropriate credentials:

import (
	"context"
	"io"
	"os"
	"path/filepath"
)

func (fs FileSystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
	// TODO: check context somehow
	return os.Open(filepath.Join(fs.Root, path))
}

How should we check the context? If we're serving this via HTTP, we could provide simple authentication by wrapping HTTP basic authentication into a context:

package server

import (
	"context"
	"net/http"
)

type authType string

func getContext(r *http.Request) context.Context {
	ctx := r.Context()
	if user, pass, ok := r.BasicAuth(); ok {
		ctx = context.WithValue(ctx, authType("user"), user)
		ctx = context.WithValue(ctx, authType("pass"), pass)
	}
	return ctx
}

func handleRequest(rw http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	doTheThing(ctx, rw)
}

...and then providing a facility to verify that context:

package server

import "context"

func (s *Server) Authenticate(ctx context.Context) (bool, error) {
	user, ok := ctx.Value(authType("user")).(string)
	if !ok {
		return false, nil
	}
	pass, ok := ctx.Value(authType("pass")).(string)
	if !ok {
		return false, nil
	}

  return s.checkCredentials(user, pass)
}

Now we know how to check the context and can successfully gate access to our files.

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"server"
)

type FileSystem struct {
	Server server.Server
	Root   string
}

func (fs *FileSystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
	ok, err := fs.Server.Authenticate(ctx)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, fmt.Errorf("%s: permission denied", path)
	}
	return os.Open(filepath.Join(fs.Root, path))
}

Unfortunately, this very tightly couples packages fs and server. If fs wants to grant users via another scheme, such as OAuth2, or to certain administrator users, or in some other way, all the methods which check for authentication must be updated. It may be that server is not something we can modify; it may be that fs isn't something we can modify.

This can be fixed by having fs accept OK types:

import (
	"context"
	"fmt"
	"io"
	"okay"
	"os"
	"path/filepath"
)

type FileSystem struct {
	Auths []okay.OK
	Root  string
}

func (fs FileSystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
	ok, err := okay.Check(ctx, path, fs.Auths...)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, fmt.Errorf("%s: permission denied", path)
	}
	return os.Open(filepath.Join(fs.Root, path))
}

Now fs and server are distinct packages coupled only by the OK interface. FileSystem.Auths can be extended at will by fs or any consumer of fs, and it will gate the guarded resources appropriately.

Use

Packages using okay should begin with the base type, and add authentication (Verify()), authorization (Allows()), and validation (Valid()) checks as needed.

The base type is always valid, and authenticates nobody and authorizes nothing.

Validation

OKs are valid until they are not, and are thereafter never valid. If a consumer wishes to create an access grant that expires (for example, to allow access to a file for only 24 hours), they should do so by creating an OK that expires after that period of time. The okay package has several helper functions for this:

ok := okay.New()
ok = okay.WithTimeout(ok, 24*time.Hour)

Or, for an OK that invalidates itself when a Context expires or is canceled:

ok := okay.New()
ok = okay.WithContext(ok, ctx)

However, consumers can implement more sophisticated behavior by implementing their own validation functions:

import (
	"os"
	"os/signal"
	"sync/atomic"
	"okay"
)

func SigintCancel() OK {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)

	var v int32

	go func() {
		<-ch
		atomic.StoreInt32(&v, 1)
	}()

	ok := okay.New()
	ok = okay.Validate(ok, func() bool {
		return atomic.LoadInt32(&v) == 0
	})
	return ok
}
Authentication

Consumers are expected to provide custom authentication with the Verify() function.

Here is a simple package that provides token-based authentication:

package authtoken

import (
	"context"
	"okay"
)

type authToken struct{}

// WithToken returns a context that contains the given auth token.
func WithToken(ctx context.Context, token string) context.Context {
	return context.WithValue(ctx, authToken{}, token)
}

// TokenOK returns an OK that verifies the given token.
func TokenOK(ok okay.OK, token string) okay.OK {
	return okay.Verify(ok, func(ctx context.Context) (bool, error) {
		tok, ok := ctx.Value(authToken{}).(string)
		if !ok {
			return false, nil
		}
		return tok == token, nil
	})
}
Authorization

Authorization is provided via the Allow() function, which allows access to resources.

The fs package (or consumers of it) might implement the following:

package fs

import (
	"strings"
	"okay"
)

func AllowFiles(ok okay.OK, file ...string) OK {
	check := make(map[string]bool)
	for _, f := range file {
		check[f] = true
	}
	return okay.Allow(ok, func(i interface{}) (bool, error) {
		f, ok := i.(string)
		if !ok {
			return false, nil
		}
		return check[f], nil
	})
}

func AllowPrefix(ok okay.OK, pfx string) OK {
	return okay.Allow(ok, func(i interface{}) (bool, error) {
		f, ok := i.(string)
		if !ok {
			return false, nil
		}
		return strings.HasPrefix(f, pfx), nil
	})
}

Documentation

Overview

Package okay defines an OK type, which can be used to gate access to arbitrary resources.

An OK is composed of three elements, used for authentication, authorization, and expiration.

Index

Constants

This section is empty.

Variables

View Source
var Invalid = errors.New("OK not valid")

Functions

func Check

func Check(ctx context.Context, resource interface{}, ok ...OK) (bool, error)

Check returns true if the given OKs are valid, verify the context, and allow the resource. It returns true if *any* of the passed OKs is valid.

func WithCancel

func WithCancel(ok OK) (OK, CancelFunc)

WithCancel returns an OK that will expire when CancelFunc is called.

Types

type CancelFunc

type CancelFunc func()

CancelFunc immediately marks the associated OK invalid. Calls after the first have no effect.

type OK

type OK interface {
	// Valid reports whether this OK is still valid.  Once an OK has been marked
	// invalid (e.g. if it has been canceled) it must not become valid again.
	Valid() bool

	// Verify reports whether the given Context has a valid credential.  If the
	// given context has invalid credentials.  ok must be false if either err is
	// non-nil, but it is valid for ok to be false when err is nil.
	Verify(ctx context.Context) (ok bool, err error)

	// Allows reports whether this OK gates access to a given asset represented
	// by the argument, such as a file path.  If err is non-nil, ok must be
	// false, but ok may be false while err is nil.
	Allows(resource interface{}) (ok bool, err error)
}

An OK represents both an authentication and an authorization guarding some resource.

func Allow

func Allow(ok OK, allow func(resource interface{}) (allowed bool, err error)) OK

Allow returns an OK that calls the provided function whenever OK.Allow() is called. Multiple such functions may be attached by successive calls to this function. The functions are called in reverse order. If *any* such function returns allowed=true, Allow() will return (true, nil). The first function to return a non-nil error will have that error returned if no function returns true. It is valid for all functions to return (false, nil).

func New

func New() OK

New returns an empty OK, which is always valid but allows nothing and verifies nobody.

func Validate

func Validate(ok OK, valid func() bool) OK

Validate returns a new OK that will call the given function every time Valid() is called. It is possible to attach many such functions by repeated application of this function. All such functions must return true for Valid() to return true.

func Verify

func Verify(ok OK, verify func(context.Context) (valid bool, err error)) OK

Verify returns a new OK that will call the given function when OK.Verify() is called. It is possible to attach multiple such functions by repeated calls to this function. Functions are called in reverse order. The first function to return valid=true will end the call chain and Valid() will return (true, nil). The first function to return a non-nil err will have that error returned if no subsequent function returns true. If *any* verify function returns true, Verify() will return (true, nil).

func WithContext

func WithContext(ok OK, ctx context.Context) OK

WithContext returns an OK that will expire when the context is canceled.

func WithDeadline

func WithDeadline(ok OK, deadline time.Time) OK

WithDeadline returns an OK that will expire once the deadline has passed.

func WithTimeout

func WithTimeout(ok OK, timeout time.Duration) OK

WithTimeout returns an OK that will expire after the given duration.

Jump to

Keyboard shortcuts

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