Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving hollow to rivets #95

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading