g8

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2021 License: MIT Imports: 6 Imported by: 0

README

g8

build Go Report Card codecov Go version Go Reference Follow TwinProduction

g8, pronounced gate, is a simple Go library for protecting HTTP handlers with tokens.

Tired of constantly re-implementing a security layer for each of applications? Me too, that's why I made g8.

Installation

go get -u github.com/TwinProduction/g8

Usage

Because the entire purpose of g8 is to NOT waste time configuring the layer of security, the primary emphasis is to keep it as simple as possible.

Simple

Just want a simple layer of security without the need for advanced permissions? This configuration is what you're looking for.

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("mytoken"))
router := http.NewServeMux()
router.Handle("/unprotected", yourHandler)
router.Handle("/protected", gate.Protect(yourHandler))
http.ListenAndServe(":8080", router)

The endpoint /protected is now only accessible if you pass the header Authorization: Bearer mytoken.

If you use http.HandleFunc instead of http.Handle, you may use gate.ProtectFunc(yourHandler) instead.

Advanced permissions

If you have tokens with more permissions than others, g8's permission system will make managing authorization a breeze.

Rather than registering tokens, think of it as registering clients, the only difference being that clients may be configured with permissions while tokens cannot.

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermission("admin")))
router := http.NewServeMux()
router.Handle("/unprotected", yourHandler)
router.Handle("/protected-with-admin", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
http.ListenAndServe(":8080", router)

The endpoint /protected-with-admin is now only accessible if you pass the header Authorization: Bearer mytoken, because the client with the token mytoken has the permission admin. Note that the following handler would also be accessible with that token:

router.Handle("/protected", gate.Protect(yourHandler))

To clarify, both clients and tokens have access to handlers that aren't protected with extra permissions, and essentially, tokens are registered as clients with no extra permissions in the background.

Creating a token like so:

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("mytoken"))

is the equivalent of creating the following client:

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken")))
With client provider

A client provider's task is to retrieve a Client from an external source (e.g. a database) when provided with a token. You should use a client provider when you have a lot of tokens and it wouldn't make sense to register all of them using AuthorizationService's WithToken/WithTokens/WithClient/WithClients.

Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4 aforementioned functions, the client provider will not be used.

clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
    // We'll assume that the following function calls your database and returns a struct "User" that 
    // has the user's token as well as the permissions granted to said user
    user := database.GetUserByToken(token)
    if user != nil {
        return g8.NewClient(user.Token).WithPermissions(user.Permissions)
    }
    return nil
})
gate := g8.NewGate(g8.NewAuthorizationService().WithClientProvider(clientProvider))

You can also configure the client provider to cache the output of the function you provide to retrieve clients by token:

clientProvider := g8.NewClientProvider(...).WithCache(ttl, maxSize)

Since g8 leverages TwinProduction/gocache, you can also use gocache's constants for configuring the TTL and the maximum size:

  • Setting the TTL to gocache.NoExpiration (-1) will disable the TTL.
  • Setting the maximum size to gocache.NoMaxSize (0) will disable the maximum cache size

If you're using a TTL and have a lot of tokens (100k+), you may want to use clientProvider.StartJanitor() to allow the cache to passively delete expired entries. If you have to re-initialize the client provider after the janitor has been started, make sure to stop the janitor first (clientProvider.StopJanitor()). This is because the janitor runs on a separate goroutine, thus, if you were to re-create a client provider and re-assign it, the old client provider would still exist in memory with the old cache. I'm only specifying this for completeness, because for the overwhelming majority of people, the gate will be created on application start and never modified again until the application shuts down, in which case, you don't even need to worry about stopping the janitor.

To avoid any misunderstandings, using a client provider is not mandatory. If you only have a few tokens and you can load them on application start, you can just leverage AuthorizationService's WithToken/WithTokens/WithClient/WithClients.

AuthorizationService

As the previous examples may have hinted, there are several ways to create clients. The one thing they have in common is that they all go through AuthorizationService, which is in charge of both managing clients and determining whether a request should be blocked or allowed through.

Function Description
WithToken Creates a single static client with no extra permissions
WithTokens Creates a slice of static clients with no extra permissions
WithClient Creates a single static client
WithClients Creates a slice of static clients
WithClientProvider Creates a client provider which will allow a fallback to a dynamic source (e.g. to a database) when a static client is not found

Except for WithClientProvider, every functions listed above can be called more than once. As a result, you may safely perform actions like this:

authorizationService := g8.NewAuthorizationService().
    WithToken("123").
    WithToken("456").
    WithClient(g8.NewClient("789").WithPermission("admin"))
gate := g8.NewGate(authorizationService)

Be aware that g8.Client supports a list of permissions as well. You may call WithPermission several times, or call WithPermissions with a slice of permissions instead.

Permissions

Unlike client permissions, handler permissions are requirements.

A client may have as many permissions as you want, but for said client to have access to a handler protected by permissions, the client must have all permissions defined by said handler in order to have access to it.

In other words, a client with the permissions create, read, update and delete would have access to all of these handlers:

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermissions([]string{"create", "read", "update", "delete"})))
router := http.NewServeMux()
router.Handle("/", gate.Protect(homeHandler)) // equivalent of gate.ProtectWithPermissions(homeHandler, []string{})
router.Handle("/create", gate.ProtectWithPermissions(createHandler, []string{"create"}))
router.Handle("/read", gate.ProtectWithPermissions(readHandler, []string{"read"}))
router.Handle("/update", gate.ProtectWithPermissions(updateHandler, []string{"update"}))
router.Handle("/delete", gate.ProtectWithPermissions(deleteHandler, []string{"delete"}))
router.Handle("/crud", gate.ProtectWithPermissions(crudHandler, []string{"create", "read", "update", "delete"}))

But it would not have access to the following handler, because while mytoken has the read permission, it does not have the backup permission:

router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"}))

Rate limiting

To add a rate limit of 100 requests per second:

gate := g8.NewGate(nil).WithRateLimit(100)

Documentation

Index

Constants

View Source
const (
	// AuthorizationHeader is the header in which g8 looks for the authorization bearer token
	AuthorizationHeader = "Authorization"

	// DefaultUnauthorizedResponseBody is the default response body returned if a request was sent with a missing or invalid token
	DefaultUnauthorizedResponseBody = "Authorization Bearer token is missing or invalid"

	// DefaultTooManyRequestsResponseBody is the default response body returned if a request exceeded the allowed rate limit
	DefaultTooManyRequestsResponseBody = "Too Many Requests"
)

Variables

View Source
var (
	// ErrNoExpiration is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start the
	// janitor despite no expiration being configured.
	// To clarify, this is because the cache janitor is only useful when an expiration is set.
	ErrNoExpiration = errors.New("no point starting the janitor if the TTL is set to not expire")

	// ErrCacheNotInitialized is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start
	// the janitor despite the cache not having been initialized using ClientProvider.WithCache
	ErrCacheNotInitialized = errors.New("cannot cache not configured")
)

Functions

This section is empty.

Types

type AuthorizationService

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

AuthorizationService is the service that manages client/token registry and client fallback as well as the service that determines whether a token meets the specific requirements to be authorized by a Gate or not.

func NewAuthorizationService

func NewAuthorizationService() *AuthorizationService

NewAuthorizationService creates a new AuthorizationService

func (*AuthorizationService) IsAuthorized

func (authorizationService *AuthorizationService) IsAuthorized(token string, permissionsRequired []string) bool

IsAuthorized checks whether a client with a given token exists and has the permissions required.

If permissionsRequired is nil or empty and a client with the given token exists, said client will have access to all handlers that are not protected by a given permission.

func (*AuthorizationService) WithClient

func (authorizationService *AuthorizationService) WithClient(client *Client) *AuthorizationService

WithClient is used to specify a single client for which authorization will be granted

When compared to WithToken, the advantage of using this function is that you may specify the client's permissions and thus, be a lot more granular with what endpoint a token has access to.

In other words, if you were to do the following:

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("12345").WithPermission("mod")))

The following handlers would be accessible with the token 12345:

router.Handle("/1st-handler", gate.ProtectWithPermissions(yourHandler, []string{"mod"}))
router.Handle("/2nd-handler", gate.Protect(yourOtherHandler))

But not this one, because the user does not have the permission "admin":

router.Handle("/3rd-handler", gate.ProtectWithPermissions(yetAnotherHandler, []string{"admin"}))

Calling this function multiple times will add multiple clients, though you may want to use WithClients instead if you plan to add multiple clients

func (*AuthorizationService) WithClientProvider

func (authorizationService *AuthorizationService) WithClientProvider(provider *ClientProvider) *AuthorizationService

WithClientProvider allows specifying a custom provider to fetch clients by token.

For example, you can use it to fallback to making a call in your database when a request is made with a token that hasn't been specified via WithToken, WithTokens, WithClient or WithClients.

func (*AuthorizationService) WithClients

func (authorizationService *AuthorizationService) WithClients(clients []*Client) *AuthorizationService

WithClients is used to specify a slice of clients for which authorization will be granted

func (*AuthorizationService) WithToken

func (authorizationService *AuthorizationService) WithToken(token string) *AuthorizationService

WithToken is used to specify a single token for which authorization will be granted

The client that will be created from this token will have access to all handlers that are not protected with a specific permission.

In other words, if you were to do the following:

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("12345"))

The following handler would be accessible with the token 12345:

router.Handle("/1st-handler", gate.Protect(yourHandler))

But not this one would not be accessible with the token 12345:

router.Handle("/2nd-handler", gate.ProtectWithPermissions(yourOtherHandler, []string{"admin"}))

Calling this function multiple times will add multiple clients, though you may want to use WithTokens instead if you plan to add multiple clients

If you wish to configure advanced permissions, consider using WithClient instead.

func (*AuthorizationService) WithTokens

func (authorizationService *AuthorizationService) WithTokens(tokens []string) *AuthorizationService

WithTokens is used to specify a slice of tokens for which authorization will be granted

type Client

type Client struct {
	// Token is the Bearer token found in the value of a Authorization header
	Token string

	// Permissions is a slice of extra permissions that may be used for more granular access control.
	//
	// If you only wish to use Gate.Protect and Gate.ProtectFunc, you do not have to worry about this,
	// since they're only used by Gate.ProtectWithPermissions and Gate.ProtectFuncWithPermissions
	Permissions []string
}

Client is a struct containing both a Token and a slice of extra Permissions that said token has.

func NewClient

func NewClient(token string) *Client

NewClient creates a Client with a given token

func NewClientWithPermissions

func NewClientWithPermissions(token string, permissions []string) *Client

NewClientWithPermissions creates a Client with a slice of permissions Equivalent to using NewClient and WithPermissions

func (Client) HasPermission

func (client Client) HasPermission(permissionRequired string) bool

HasPermission checks whether a client has a given permission

func (Client) HasPermissions

func (client Client) HasPermissions(permissionsRequired []string) bool

HasPermissions checks whether a client has the all permissions passed

func (*Client) WithPermission

func (client *Client) WithPermission(permission string) *Client

WithPermission adds a permission to a client

func (*Client) WithPermissions

func (client *Client) WithPermissions(permissions []string) *Client

WithPermissions adds a slice of permissions to a client

type ClientProvider

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

ClientProvider has the task of retrieving a Client from an external source (e.g. a database) when provided with a token. It should be used when you have a lot of tokens and it wouldn't make sense to register all of them using AuthorizationService's WithToken, WithTokens, WithClient or WithClients.

Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4 aforementioned functions, the client provider will not be used by the AuthorizationService when a request is made with said token. It will, however, be called upon if a token that is not explicitly registered in AuthorizationService is sent alongside a request going through the Gate.

clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
    // We'll assume that the following function calls your database and returns a struct "User" that
    // has the user's token as well as the permissions granted to said user
    user := database.GetUserByToken(token)
    if user != nil {
        return g8.NewClient(user.Token).WithPermissions(user.Permissions)
    }
    return nil
})
gate := g8.NewGate(g8.NewAuthorizationService().WithClientProvider(clientProvider))

func NewClientProvider

func NewClientProvider(getClientByTokenFunc func(token string) *Client) *ClientProvider

NewClientProvider creates a ClientProvider The parameter that must be passed is a function that the provider will use to retrieve a client by a given token

Example:

clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
    // We'll assume that the following function calls your database and returns a struct "User" that
    // has the user's token as well as the permissions granted to said user
    user := database.GetUserByToken(token)
    if user != nil {
        return g8.NewClient(user.Token).WithPermissions(user.Permissions)
    }
    return nil
})
gate := g8.NewGate(g8.NewAuthorizationService().WithClientProvider(clientProvider))

func (*ClientProvider) GetClientByToken

func (provider *ClientProvider) GetClientByToken(token string) *Client

GetClientByToken retrieves a client by its token through the provided getClientByTokenFunc.

func (*ClientProvider) StartCacheJanitor added in v0.2.0

func (provider *ClientProvider) StartCacheJanitor() error

StartCacheJanitor starts the cache janitor, which passively deletes expired cache entries in the background.

Not really necessary unless you have a lot of clients (100000+).

Even without the janitor, active eviction will still happen (i.e. when GetClientByToken is called, but the cache entry for the given token has expired, the cache entry will be automatically deleted and re-fetched from the user-defined getClientByTokenFunc)

func (*ClientProvider) StopCacheJanitor added in v0.2.0

func (provider *ClientProvider) StopCacheJanitor()

StopCacheJanitor stops the cache janitor

Not required unless your application initializes multiple providers over the course of its lifecycle. In English, that means if you initialize a ClientProvider only once on application start and it stays up until your application shuts down, you don't need to call this function.

func (*ClientProvider) WithCache added in v0.2.0

func (provider *ClientProvider) WithCache(ttl time.Duration, maxSize int) *ClientProvider

WithCache adds cache options to the ClientProvider.

ttl is the time until the cache entry will expire. A TTL of gocache.NoExpiration (-1) means no expiration maxSize is the maximum amount of entries that can be in the cache at any given time. If a value of gocache.NoMaxSize (0) or less is provided for maxSize, there will be no maximum size.

Example:

clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
    // We'll assume that the following function calls your database and returns a struct "User" that
    // has the user's token as well as the permissions granted to said user
    user := database.GetUserByToken(token)
    if user != nil {
        return g8.NewClient(user.Token).WithPermissions(user.Permissions)
    }
    return nil
})
gate := g8.NewGate(g8.NewAuthorizationService().WithClientProvider(clientProvider.WithCache(time.Hour, 70000)))

type Gate

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

Gate is lock to the front door of your API, letting only those you allow through.

func NewGate

func NewGate(authorizationService *AuthorizationService) *Gate

NewGate creates a new Gate.

func (*Gate) Protect

func (gate *Gate) Protect(handler http.Handler) http.Handler

Protect secures a handler, requiring requests going through to have a valid Authorization Bearer token. Unlike ProtectWithPermissions, Protect will allow access to any registered tokens, regardless of their permissions or lack thereof.

Example:

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("token"))
router := http.NewServeMux()
// Without protection
router.Handle("/handle", yourHandler)
// With protection
router.Handle("/handle", gate.Protect(yourHandler))

func (*Gate) ProtectFunc

func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc

ProtectFunc secures a handlerFunc, requiring requests going through to have a valid Authorization Bearer token. Unlike ProtectFuncWithPermissions, ProtectFunc will allow access to any registered tokens, regardless of their permissions or lack thereof.

Example:

gate := g8.NewGate(g8.NewAuthorizationService().WithToken("token"))
router := http.NewServeMux()
// Without protection
router.HandleFunc("/handle", yourHandlerFunc)
// With protection
router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))

func (*Gate) ProtectFuncWithPermission added in v0.1.0

func (gate *Gate) ProtectFuncWithPermission(handlerFunc http.HandlerFunc, permission string) http.HandlerFunc

ProtectFuncWithPermission does the same thing as ProtectFuncWithPermissions, but for a single permission instead of a slice of permissions

See ProtectFuncWithPermissions for further documentation

func (*Gate) ProtectFuncWithPermissions

func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permissions []string) http.HandlerFunc

ProtectFuncWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer token as well as a slice of permissions that must be met.

Example:

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
router := http.NewServeMux()
// Without protection
router.HandleFunc("/handle", yourHandlerFunc)
// With protection
router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))

func (*Gate) ProtectWithPermission added in v0.1.0

func (gate *Gate) ProtectWithPermission(handler http.Handler, permission string) http.Handler

ProtectWithPermission does the same thing as ProtectWithPermissions, but for a single permission instead of a slice of permissions

See ProtectWithPermissions for further documentation

func (*Gate) ProtectWithPermissions

func (gate *Gate) ProtectWithPermissions(handler http.Handler, permissions []string) http.Handler

ProtectWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer token as well as a slice of permissions that must be met.

Example:

gate := g8.NewGate(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
router := http.NewServeMux()
// Without protection
router.Handle("/handle", yourHandler)
// With protection
router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))

func (*Gate) WithCustomUnauthorizedResponseBody

func (gate *Gate) WithCustomUnauthorizedResponseBody(unauthorizedResponseBody []byte) *Gate

WithCustomUnauthorizedResponseBody sets a custom response body when Gate determines that a request must be blocked

func (*Gate) WithRateLimit added in v1.1.0

func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate

WithRateLimit adds rate limiting to the Gate

type RateLimiter added in v1.1.0

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

RateLimiter is a fixed rate limiter

func NewRateLimiter added in v1.1.0

func NewRateLimiter(maximumExecutionsPerSecond int) *RateLimiter

NewRateLimiter creates a RateLimiter

func (*RateLimiter) Try added in v1.1.0

func (r *RateLimiter) Try() bool

Try updates the number of executions if the rate limit quota hasn't been reached and returns whether the attempt was successful or not.

Returns false if the execution was not successful (rate limit quota has been reached) Returns true if the execution was successful (rate limit quota has not been reached)

Jump to

Keyboard shortcuts

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