Skip to content

Commit

Permalink
Moving hollow to rivets (#95)
Browse files Browse the repository at this point in the history
* Moving hollow to rivets

* lint fixes, change fleetdb reference to main

* fix github workflow

* remove 2m timeout
  • Loading branch information
jakeschuurmans authored Sep 13, 2024
1 parent 8f6e267 commit 6a13f25
Show file tree
Hide file tree
Showing 27 changed files with 3,019 additions and 157 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/push-pr-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:

- name: Test
run: go test ./...
with:
args: -tags testtools

build:
runs-on: ubuntu-latest
Expand Down
14 changes: 8 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
service:
golangci-lint-version: 1.55.2 # use the fixed version to not introduce new linters unexpectedly

run:
build-tags:
- testtools

linters-settings:
govet:
enable:
auto-fix: true
check-shadowing: true
settings:
printf:
funcs:
Expand Down Expand Up @@ -70,12 +73,9 @@ linters:
enable-all: false
disable-all: true

run:
# build-tags:
skip-dirs:
- internal/fixtures

issues:
exclude-dirs:
- internal/fixtures
exclude-rules:
- linters:
- gosec
Expand Down Expand Up @@ -108,3 +108,5 @@ issues:
# EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
- Potential file inclusion via variable
exclude-use-default: false

shadow: true
13 changes: 0 additions & 13 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
testonly: False
with-expecter: True
packages:
github.com/metal-toolbox/rivets/events/controller:
config:
dir: events/controller
fileName: "mock_{{.InterfaceName | firstLower}}.go"
inpackage: True
interfaces:
TaskHandler:
Publisher:
StatusPublisher:
ConditionStatusQueryor:
ConditionStatusPublisher:
eventStatusAcknowleger:
LivenessCheckin:
github.com/metal-toolbox/rivets/events:
config:
dir: events/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ lint:

## Go test
test: lint
CGO_ENABLED=0 go test -timeout 1m -v -covermode=atomic ./...
CGO_ENABLED=0 go test -tags testtools -timeout 1m -v -covermode=atomic ./...

## Generate mocks
gen-mock:
Expand Down
2 changes: 2 additions & 0 deletions ginauth/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package ginauth provides a authentication and authorization middleware for use with a gin server
package ginauth
104 changes: 104 additions & 0 deletions ginauth/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package ginauth

import (
"errors"
"fmt"
"net/http"
)

var (
// ErrInvalidMiddlewareReference the middleware added was invalid
ErrInvalidMiddlewareReference = errors.New("invalid middleware")

// ErrMiddlewareRemote is the error returned when the middleware couldn't contact the remote endpoint
ErrMiddlewareRemote = errors.New("middleware setup")

// ErrAuthentication defines a generic authentication error. This specifies that we couldn't
// validate a token for some reason. This is not to be used as-is but is useful for type
// comparison with the `AuthError` struct.
ErrAuthentication = errors.New("authentication error")

// ErrInvalidSigningKey is the error returned when a token can not be verified because the signing key in invalid
// NOTE(jaosorior): The fact that this is in this package is a little hacky... but it's to not have a
// circular dependency with the ginjwt package.
ErrInvalidSigningKey = errors.New("invalid token signing key")
)

// AuthError represents an auth error coming from a middleware function
type AuthError struct {
HTTPErrorCode int
err error
}

// NewAuthenticationError returns an authentication error which is due
// to not being able to determine who's the requestor (e.g. authentication error)
func NewAuthenticationError(msg string) *AuthError {
return &AuthError{
HTTPErrorCode: http.StatusUnauthorized,
//nolint:goerr113 // it must be dynamic here
err: errors.New(msg),
}
}

// NewAuthenticationErrorFrom returns an authentication error which is due
// to not being able to determine who's the requestor (e.g. authentication error).
// The error is based on another one (it wraps it).
func NewAuthenticationErrorFrom(err error) *AuthError {
return &AuthError{
HTTPErrorCode: http.StatusUnauthorized,
err: err,
}
}

// NewAuthorizationError returns an authorization error which is due to
// not being able to determine what the requestor can do (e.g. authorization error)
func NewAuthorizationError(msg string) *AuthError {
return &AuthError{
HTTPErrorCode: http.StatusForbidden,
//nolint:goerr113 // it must be dynamic here
err: errors.New(msg),
}
}

// Error ensures AuthenticationError implements the error interface
func (ae *AuthError) Error() string {
return ae.err.Error()
}

// Unwrap ensures that we're able to verify that this is indeed
// an authentication error
func (ae *AuthError) Unwrap() error {
return ErrAuthentication
}

// TokenValidationError specifies that there was an authentication error
// due to the token being invalid
type TokenValidationError struct {
AuthError
}

// Error ensures AuthenticationError implements the error interface
func (tve *TokenValidationError) Error() string {
return fmt.Sprintf("invalid auth token: %s", &tve.AuthError)
}

// Unwrap allows TokenValidationError to be detected as an AuthError.
func (tve *TokenValidationError) Unwrap() error {
return &tve.AuthError
}

// NewTokenValidationError returns a TokenValidationError that wraps the given error
func NewTokenValidationError(err error) error {
return &TokenValidationError{
AuthError: AuthError{
HTTPErrorCode: http.StatusUnauthorized,
err: err,
},
}
}

// NewInvalidSigningKeyError returns an AuthError that indicates
// that the signing key used to validate the token was not valid
func NewInvalidSigningKeyError() error {
return NewAuthenticationErrorFrom(ErrInvalidSigningKey)
}
20 changes: 20 additions & 0 deletions ginauth/genericmiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ginauth

import (
"github.com/gin-gonic/gin"
)

// ClaimMetadata returns the minimal relevant information so middleware
// can set the appropriate metadata to a context (e.g. a gin.Context)
type ClaimMetadata struct {
Subject string
User string
Roles []string
}

// GenericAuthMiddleware defines middleware that verifies a token coming from a gin.Context.
// Note that this can be stacked together using the MultiTokenMiddleware construct.
type GenericAuthMiddleware interface {
VerifyTokenWithScopes(*gin.Context, []string) (ClaimMetadata, error)
SetMetadata(*gin.Context, ClaimMetadata)
}
25 changes: 25 additions & 0 deletions ginauth/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ginauth

import (
"errors"
"net/http"

"github.com/gin-gonic/gin"
)

// AbortBecauseOfError aborts a gin context based on a given error
func AbortBecauseOfError(c *gin.Context, err error) {
var authErr *AuthError

var validationErr *TokenValidationError

switch {
case errors.As(err, &validationErr):
c.AbortWithStatusJSON(validationErr.HTTPErrorCode, gin.H{"message": "invalid auth token", "error": validationErr.Error()})
case errors.As(err, &authErr):
c.AbortWithStatusJSON(authErr.HTTPErrorCode, gin.H{"message": authErr.Error()})
default:
// If we can't cast it, unauthorize anyway
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": err.Error()})
}
}
100 changes: 100 additions & 0 deletions ginauth/multitokenmiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ginauth

import (
"errors"
"fmt"
"sync"

"github.com/gin-gonic/gin"
)

// MultiTokenMiddleware Allows for concurrently verifying a token
// using different middleware implementations. This relies on implementing
// the GenericAuthMiddleware interface.
// Only the first detected success will be taken into account.
// Note that middleware objects don't have to be of Middleware type, that's
// only one object that implements the interface.
type MultiTokenMiddleware struct {
verifiers []GenericAuthMiddleware
}

// NewMultiTokenMiddleware builds a MultiTokenMiddleware object from multiple AuthConfigs.
func NewMultiTokenMiddleware() (*MultiTokenMiddleware, error) {
mtm := &MultiTokenMiddleware{}
mtm.verifiers = make([]GenericAuthMiddleware, 0)

return mtm, nil
}

// Add will append another middleware object (or verifier) to the list
// which we'll use to check concurrently
func (mtm *MultiTokenMiddleware) Add(middleware GenericAuthMiddleware) error {
if middleware == nil {
return fmt.Errorf("%w: %s", ErrInvalidMiddlewareReference, "The middleware reference can't be nil")
}

mtm.verifiers = append(mtm.verifiers, middleware)

return nil
}

// AuthRequired is similar to the `AuthRequired` function from the Middleware type
// in the sense that it'll evaluate the scopes and the token coming from the context.
// However, this will concurrently evaluate them with the middlewares configured in this
// struct
func (mtm *MultiTokenMiddleware) AuthRequired(scopes []string) gin.HandlerFunc {
return func(c *gin.Context) {
var wg sync.WaitGroup

res := make(chan error, len(mtm.verifiers))

wg.Add(len(mtm.verifiers))

for _, verifier := range mtm.verifiers {
go func(v GenericAuthMiddleware, c *gin.Context, r chan<- error) {
defer wg.Done()

cm, err := v.VerifyTokenWithScopes(c, scopes)

if err != nil {
v.SetMetadata(c, cm)
}

r <- err
}(verifier, c, res)
}

wg.Wait()
close(res)

var surfacingErr error

for err := range res {
if err == nil {
// NOTE(jaosorior): This takes the first non-error as a success.
// It would be quite strange if we would get multiple successes.
return
}

// initialize surfacingErr.
if surfacingErr == nil {
surfacingErr = err
continue
}

// If we previously had an error related to having an invalid signing key
// we overwrite the error to be surfaced. We care more about other types of
// errors, such as not having the appropriate scope
// Also, if we previously had an error with the remote endpoint, we override the error.
// This might be a very general error and more specific ones are preferred
// for surfacing.
if errors.Is(surfacingErr, ErrMiddlewareRemote) || errors.Is(surfacingErr, ErrInvalidSigningKey) {
surfacingErr = err
}
}

if surfacingErr != nil {
AbortBecauseOfError(c, surfacingErr)
}
}
}
Loading

0 comments on commit 6a13f25

Please sign in to comment.