From 7fe83f90951ed1f0ccad2be9c654f3d373eef232 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 10 Nov 2024 10:45:40 +0100 Subject: [PATCH] Prepare model and config for more token type validators like OIDC Signed-off-by: Xabier Larrakoetxea --- internal/app/auth/auth.go | 4 +- internal/app/auth/auth_test.go | 32 +++++++++------- internal/app/auth/authmock/token_getter.go | 14 +++---- internal/app/auth/token_validate.go | 28 +++++++------- internal/model/model.go | 14 ++++--- internal/storage/memory/mapper.go | 14 ++++--- internal/storage/memory/memory.go | 6 +-- internal/storage/memory/memory_test.go | 44 ++++++++++++---------- pkg/api/v1/v1.go | 17 ++++++--- 9 files changed, 97 insertions(+), 76 deletions(-) diff --git a/internal/app/auth/auth.go b/internal/app/auth/auth.go index 6caee4d..a266ad9 100644 --- a/internal/app/auth/auth.go +++ b/internal/app/auth/auth.go @@ -12,7 +12,7 @@ import ( ) type TokenGetter interface { - GetToken(ctx context.Context, tokenValue string) (*model.Token, error) + GetStaticTokenValidation(ctx context.Context, tokenValue string) (*model.StaticTokenValidation, error) } //go:generate mockery --case underscore --output authmock --outpkg authmock --name TokenGetter @@ -68,7 +68,7 @@ func (s Service) Authenticate(ctx context.Context, req AuthenticateRequest) (res logger := s.logger.WithValues(log.Kv{"url": req.Review.HTTPURL, "method": req.Review.HTTPMethod}) // Get token and its properties. - token, err := s.tokenGetter.GetToken(ctx, req.Review.Token) + token, err := s.tokenGetter.GetStaticTokenValidation(ctx, req.Review.Token) if err != nil { if errors.Is(err, internalerrors.ErrNotFound) { logger.Infof("Unknown token") diff --git a/internal/app/auth/auth_test.go b/internal/app/auth/auth_test.go index a015922..95580b4 100644 --- a/internal/app/auth/auth_test.go +++ b/internal/app/auth/auth_test.go @@ -35,7 +35,7 @@ func TestServiceAuth(t *testing.T) { "A token review with fails while getting the token it should fail.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "sometoken").Once().Return(nil, fmt.Errorf("something")) + mtg.On("GetStaticTokenValidation", mock.Anything, "sometoken").Once().Return(nil, fmt.Errorf("something")) }, req: auth.AuthenticateRequest{Review: model.TokenReview{ Token: "sometoken", @@ -45,7 +45,7 @@ func TestServiceAuth(t *testing.T) { "A token review with a valid token that is missing, should not return as authenticated.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "missing").Once().Return(nil, internalerrors.ErrNotFound) + mtg.On("GetStaticTokenValidation", mock.Anything, "missing").Once().Return(nil, internalerrors.ErrNotFound) }, req: auth.AuthenticateRequest{Review: model.TokenReview{ Token: "missing", @@ -55,9 +55,11 @@ func TestServiceAuth(t *testing.T) { "A token review that is disabled should be invalid.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "token0").Once().Return(&model.Token{ - Value: "token0", - Disable: true, + mtg.On("GetStaticTokenValidation", mock.Anything, "token0").Once().Return(&model.StaticTokenValidation{ + Value: "token0", + Common: model.TokenCommon{ + Disable: true, + }, }, nil) }, req: auth.AuthenticateRequest{Review: model.TokenReview{ @@ -68,7 +70,7 @@ func TestServiceAuth(t *testing.T) { "A token review that has expired should be invalid.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "token0").Once().Return(&model.Token{ + mtg.On("GetStaticTokenValidation", mock.Anything, "token0").Once().Return(&model.StaticTokenValidation{ Value: "token0", ExpiresAt: time.Now().Add(-24 * time.Hour), }, nil) @@ -81,9 +83,11 @@ func TestServiceAuth(t *testing.T) { "A token review with an invalid URL should be invalid.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "token0").Once().Return(&model.Token{ - Value: "token0", - AllowedURL: regexp.MustCompile("https://something.com/.*"), + mtg.On("GetStaticTokenValidation", mock.Anything, "token0").Once().Return(&model.StaticTokenValidation{ + Value: "token0", + Common: model.TokenCommon{ + AllowedURL: regexp.MustCompile("https://something.com/.*"), + }, }, nil) }, req: auth.AuthenticateRequest{Review: model.TokenReview{ @@ -95,9 +99,11 @@ func TestServiceAuth(t *testing.T) { "A token review with an invalid method should be invalid.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "token0").Once().Return(&model.Token{ - Value: "token0", - AllowedMethod: regexp.MustCompile("POST"), + mtg.On("GetStaticTokenValidation", mock.Anything, "token0").Once().Return(&model.StaticTokenValidation{ + Value: "token0", + Common: model.TokenCommon{ + AllowedMethod: regexp.MustCompile("POST"), + }, }, nil) }, req: auth.AuthenticateRequest{Review: model.TokenReview{ @@ -109,7 +115,7 @@ func TestServiceAuth(t *testing.T) { "A token review that is valid, should be authenticated.": { mock: func(mtg *authmock.TokenGetter) { - mtg.On("GetToken", mock.Anything, "token0").Once().Return(&model.Token{ + mtg.On("GetStaticTokenValidation", mock.Anything, "token0").Once().Return(&model.StaticTokenValidation{ Value: "token0", ClientID: "client0", }, nil) diff --git a/internal/app/auth/authmock/token_getter.go b/internal/app/auth/authmock/token_getter.go index ba4e65e..4764a2e 100644 --- a/internal/app/auth/authmock/token_getter.go +++ b/internal/app/auth/authmock/token_getter.go @@ -14,24 +14,24 @@ type TokenGetter struct { mock.Mock } -// GetToken provides a mock function with given fields: ctx, tokenValue -func (_m *TokenGetter) GetToken(ctx context.Context, tokenValue string) (*model.Token, error) { +// GetStaticTokenValidation provides a mock function with given fields: ctx, tokenValue +func (_m *TokenGetter) GetStaticTokenValidation(ctx context.Context, tokenValue string) (*model.StaticTokenValidation, error) { ret := _m.Called(ctx, tokenValue) if len(ret) == 0 { - panic("no return value specified for GetToken") + panic("no return value specified for GetStaticTokenValidation") } - var r0 *model.Token + var r0 *model.StaticTokenValidation var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*model.Token, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*model.StaticTokenValidation, error)); ok { return rf(ctx, tokenValue) } - if rf, ok := ret.Get(0).(func(context.Context, string) *model.Token); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *model.StaticTokenValidation); ok { r0 = rf(ctx, tokenValue) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Token) + r0 = ret.Get(0).(*model.StaticTokenValidation) } } diff --git a/internal/app/auth/token_validate.go b/internal/app/auth/token_validate.go index e3d7818..eed8da3 100644 --- a/internal/app/auth/token_validate.go +++ b/internal/app/auth/token_validate.go @@ -22,17 +22,17 @@ type reviewResult struct { // Authenticater knows how to authenticate. type authenticater interface { - Authenticate(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) + Authenticate(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) } -type authenticaterFunc func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) +type authenticaterFunc func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) -func (a authenticaterFunc) Authenticate(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { +func (a authenticaterFunc) Authenticate(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { return a(ctx, r, t) } func newAuthenticaterChain(auths ...authenticater) authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { var res *reviewResult var err error for _, a := range auths { @@ -52,7 +52,7 @@ func newAuthenticaterChain(auths ...authenticater) authenticater { } func newTokenExistAuthenticator() authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { if r.Token == t.Value { return &reviewResult{Valid: true}, nil } @@ -62,7 +62,7 @@ func newTokenExistAuthenticator() authenticater { } func newNotExpiredAuthenticator() authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { if t.ExpiresAt.IsZero() { return &reviewResult{Valid: true}, nil } @@ -76,12 +76,12 @@ func newNotExpiredAuthenticator() authenticater { } func newValidMethodAuthenticator() authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { - if t.AllowedMethod == nil { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { + if t.Common.AllowedMethod == nil { return &reviewResult{Valid: true}, nil } - if t.AllowedMethod.MatchString(r.HTTPMethod) { + if t.Common.AllowedMethod.MatchString(r.HTTPMethod) { return &reviewResult{Valid: true}, nil } @@ -90,12 +90,12 @@ func newValidMethodAuthenticator() authenticater { } func newValidURLAuthenticator() authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { - if t.AllowedURL == nil { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { + if t.Common.AllowedURL == nil { return &reviewResult{Valid: true}, nil } - if t.AllowedURL.MatchString(r.HTTPURL) { + if t.Common.AllowedURL.MatchString(r.HTTPURL) { return &reviewResult{Valid: true}, nil } @@ -104,8 +104,8 @@ func newValidURLAuthenticator() authenticater { } func newDisabledAuthenticator() authenticater { - return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.Token) (*reviewResult, error) { - if !t.Disable { + return authenticaterFunc(func(ctx context.Context, r model.TokenReview, t model.StaticTokenValidation) (*reviewResult, error) { + if !t.Common.Disable { return &reviewResult{Valid: true}, nil } diff --git a/internal/model/model.go b/internal/model/model.go index 3409044..22f3587 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -5,13 +5,17 @@ import ( "time" ) -// Token represents an auth token that can be used to validate an authentication. +// StaticTokenValidation represents an auth static token information that can be used to validate an authentication. // Token value generation example: `openssl rand -base64 32`. -type Token struct { - Value string - ClientID string +type StaticTokenValidation struct { + Value string + ClientID string + ExpiresAt time.Time + Common TokenCommon +} + +type TokenCommon struct { Disable bool - ExpiresAt time.Time AllowedURL *regexp.Regexp AllowedMethod *regexp.Regexp } diff --git a/internal/storage/memory/mapper.go b/internal/storage/memory/mapper.go index 6738f7b..3961036 100644 --- a/internal/storage/memory/mapper.go +++ b/internal/storage/memory/mapper.go @@ -13,7 +13,7 @@ import ( apiv1 "github.com/slok/simple-ingress-external-auth/pkg/api/v1" ) -func mapJSONV1ToModel(data string) (map[string]model.Token, error) { +func mapJSONV1ToModel(data string) (map[string]model.StaticTokenValidation, error) { // Substitute env vars in the required strings. envedData, err := envsubst.EvalEnv(data) if err != nil { @@ -35,7 +35,7 @@ func mapJSONV1ToModel(data string) (map[string]model.Token, error) { } // Map. - tokens := map[string]model.Token{} + tokens := map[string]model.StaticTokenValidation{} for _, t := range c1.Tokens { if t.Value == "" { return nil, fmt.Errorf("token value can't be empty") @@ -46,11 +46,13 @@ func mapJSONV1ToModel(data string) (map[string]model.Token, error) { expiresAt = *t.ExpiresAt } - token := model.Token{ + token := model.StaticTokenValidation{ Value: t.Value, ClientID: t.ClientID, - Disable: t.Disable, ExpiresAt: expiresAt, + Common: model.TokenCommon{ + Disable: t.Disable, + }, } if t.AllowedMethodRegex != "" { @@ -58,7 +60,7 @@ func mapJSONV1ToModel(data string) (map[string]model.Token, error) { if err != nil { return nil, fmt.Errorf("could not compile %s regex: %w", t.AllowedMethodRegex, err) } - token.AllowedMethod = r + token.Common.AllowedMethod = r } if t.AllowedURLRegex != "" { @@ -66,7 +68,7 @@ func mapJSONV1ToModel(data string) (map[string]model.Token, error) { if err != nil { return nil, fmt.Errorf("could not compile %s regex: %w", t.AllowedURLRegex, err) } - token.AllowedURL = r + token.Common.AllowedURL = r } // Check same token is not twice. diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index edb671d..dcd52ac 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -10,7 +10,7 @@ import ( ) type TokenRepository struct { - tokens map[string]model.Token + tokens map[string]model.StaticTokenValidation } func NewTokenRepository(logger log.Logger, config string) (*TokenRepository, error) { @@ -19,12 +19,12 @@ func NewTokenRepository(logger log.Logger, config string) (*TokenRepository, err return nil, err } - logger.WithValues(log.Kv{"svc": "memory.TokenRepository", "tokens": len(tokens)}).Infof("Tokens loaded") + logger.WithValues(log.Kv{"svc": "memory.TokenRepository", "tokens": len(tokens)}).Infof("Token validations loaded") return &TokenRepository{tokens: tokens}, nil } -func (t TokenRepository) GetToken(ctx context.Context, tokenValue string) (*model.Token, error) { +func (t TokenRepository) GetStaticTokenValidation(ctx context.Context, tokenValue string) (*model.StaticTokenValidation, error) { token, ok := t.tokens[tokenValue] if !ok { return nil, fmt.Errorf("token not found: %w", internalerrors.ErrNotFound) diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index 6dda8a5..32f3b5d 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -57,12 +57,12 @@ tokens: ` ) -func TestTokenRepositoryGetToken(t *testing.T) { +func TestTokenRepositoryGetStaticTokenValidation(t *testing.T) { tests := map[string]struct { config string env map[string]string token string - expToken *model.Token + expToken *model.StaticTokenValidation expErr bool }{ "If the token is missing, it should fail": { @@ -74,7 +74,7 @@ func TestTokenRepositoryGetToken(t *testing.T) { "An existing token, should be returned (basic)": { config: goodJSONConfig, token: "t0", - expToken: &model.Token{ + expToken: &model.StaticTokenValidation{ Value: "t0", ClientID: "c0", }, @@ -83,26 +83,30 @@ func TestTokenRepositoryGetToken(t *testing.T) { "An existing token, should be returned (full)": { config: goodJSONConfig, token: "t1", - expToken: &model.Token{ - Value: "t1", - ClientID: "c1", - Disable: true, - ExpiresAt: time.Date(2022, time.Month(7), 4, 14, 21, 22, 520000000, time.UTC), - AllowedURL: regexp.MustCompile(`https://custom.host.slok.dev/.*`), - AllowedMethod: regexp.MustCompile(`(GET|POST)`), + expToken: &model.StaticTokenValidation{ + Value: "t1", + ClientID: "c1", + ExpiresAt: time.Date(2022, time.Month(7), 4, 14, 21, 22, 520000000, time.UTC), + Common: model.TokenCommon{ + Disable: true, + AllowedURL: regexp.MustCompile(`https://custom.host.slok.dev/.*`), + AllowedMethod: regexp.MustCompile(`(GET|POST)`), + }, }, }, "An existing token, should be returned (full YAML)": { config: goodYAMLConfig, token: "t1", - expToken: &model.Token{ - Value: "t1", - ClientID: "c1", - Disable: true, - ExpiresAt: time.Date(2022, time.Month(7), 4, 14, 21, 22, 520000000, time.UTC), - AllowedURL: regexp.MustCompile(`https://custom.host.slok.dev/.*`), - AllowedMethod: regexp.MustCompile(`(GET|POST)`), + expToken: &model.StaticTokenValidation{ + Value: "t1", + ClientID: "c1", + ExpiresAt: time.Date(2022, time.Month(7), 4, 14, 21, 22, 520000000, time.UTC), + Common: model.TokenCommon{ + Disable: true, + AllowedURL: regexp.MustCompile(`https://custom.host.slok.dev/.*`), + AllowedMethod: regexp.MustCompile(`(GET|POST)`), + }, }, }, @@ -121,7 +125,7 @@ func TestTokenRepositoryGetToken(t *testing.T) { } `, token: "1234567890", - expToken: &model.Token{ + expToken: &model.StaticTokenValidation{ Value: "1234567890", }, }, @@ -136,7 +140,7 @@ tokens: - value: ${TEST_TOKEN} `, token: "1234567890", - expToken: &model.Token{ + expToken: &model.StaticTokenValidation{ Value: "1234567890", }, }, @@ -160,7 +164,7 @@ tokens: repo, err := memory.NewTokenRepository(log.Noop, test.config) require.NoError(err) - token, err := repo.GetToken(context.TODO(), test.token) + token, err := repo.GetStaticTokenValidation(context.TODO(), test.token) if test.expErr { assert.Error(err) diff --git a/pkg/api/v1/v1.go b/pkg/api/v1/v1.go index 30cc76b..d8e5fba 100644 --- a/pkg/api/v1/v1.go +++ b/pkg/api/v1/v1.go @@ -7,11 +7,16 @@ type Config struct { Tokens []Token `json:"tokens"` } +type Common struct { + Disable bool `json:"disable,omitempty"` + AllowedURLRegex string `json:"allowed_url,omitempty"` + AllowedMethodRegex string `json:"allowed_method,omitempty"` +} + type Token struct { - Value string `json:"value"` - ClientID string `json:"client_id"` - Disable bool `json:"disable,omitempty"` - ExpiresAt *time.Time `json:"expires_at,omitempty"` - AllowedURLRegex string `json:"allowed_url,omitempty"` - AllowedMethodRegex string `json:"allowed_method,omitempty"` + Common + + Value string `json:"value"` + ClientID string `json:"client_id"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` }