feat: Implementation of AuthorizerV1

The `AuthorizerV1` defines the behavior for authorizing an InfluxDB
1.x API using `CredentialsV1`. These credentials are extracted from
an API, such as the Authorization header of a HTTP request.
pull/19875/head
Stuart Carnie 2020-10-30 10:06:34 -07:00
parent 0e0a784e29
commit 3fbbd6afcf
7 changed files with 664 additions and 0 deletions

37
credentials.go Normal file
View File

@ -0,0 +1,37 @@
package influxdb
import "context"
var (
// ErrCredentialsUnauthorized is the error returned when CredentialsV1 cannot be
// authorized.
ErrCredentialsUnauthorized = &Error{
Code: EUnauthorized,
Msg: "Unauthorized",
}
)
// SchemeV1 is an enumeration of supported authorization types
type SchemeV1 string
const (
// SchemeV1Basic indicates the credentials came from an Authorization header using the BASIC scheme
SchemeV1Basic SchemeV1 = "basic"
// SchemeToken indicates the credentials came from an Authorization header using the Token scheme
SchemeV1Token SchemeV1 = "token"
// SchemeURL indicates the credentials came from the u and p query parameters
SchemeV1URL SchemeV1 = "url"
)
// CredentialsV1 encapsulates the required credentials to authorize a v1 HTTP request.
type CredentialsV1 struct {
Scheme SchemeV1
Username string
Token string
}
type AuthorizerV1 interface {
Authorize(ctx context.Context, v1 CredentialsV1) (*Authorization, error)
}

15
mock/authorizer_v1.go Normal file
View File

@ -0,0 +1,15 @@
package mock
import (
"context"
"github.com/influxdata/influxdb/v2"
)
type AuthorizerV1 struct {
AuthorizeFn func(ctx context.Context, c influxdb.CredentialsV1) (*influxdb.Authorization, error)
}
func (a *AuthorizerV1) Authorize(ctx context.Context, c influxdb.CredentialsV1) (*influxdb.Authorization, error) {
return a.AuthorizeFn(ctx, c)
}

View File

@ -0,0 +1,124 @@
package authorization
import (
"context"
"errors"
"github.com/influxdata/influxdb/v2"
)
var (
ErrUnsupportedScheme = &influxdb.Error{
Code: influxdb.EInternal,
Msg: "unsupported authorization scheme",
}
)
type UserFinder interface {
// Returns a single user by ID.
FindUserByID(ctx context.Context, id influxdb.ID) (*influxdb.User, error)
}
type PasswordComparer interface {
ComparePassword(ctx context.Context, authID influxdb.ID, password string) error
}
type AuthTokenFinder interface {
FindAuthorizationByToken(ctx context.Context, token string) (*influxdb.Authorization, error)
}
// A type that is used to verify credentials.
type Authorizer struct {
AuthV1 AuthTokenFinder // A service to find V1 tokens
AuthV2 AuthTokenFinder // A service to find V2 tokens
Comparer PasswordComparer // A service to compare passwords for V1 tokens
User UserFinder // A service to find users
}
// Authorize returns an influxdb.Authorization if c can be verified; otherwise, an error.
// influxdb.ErrCredentialsUnauthorized will be returned if the credentials are invalid.
func (v *Authorizer) Authorize(ctx context.Context, c influxdb.CredentialsV1) (auth *influxdb.Authorization, err error) {
// the defer function provides the following guarantees:
// * the authorization token status is active and
// * the user status is active
defer func() {
if err != nil {
return
}
if auth == nil {
return
}
if auth.Status != influxdb.Active {
auth, err = nil, influxdb.ErrCredentialsUnauthorized
return
}
// check the user is still active
if user, userErr := v.User.FindUserByID(ctx, auth.UserID); err != nil {
auth, err = nil, v.normalizeError(userErr)
return
} else if user == nil || user.Status != influxdb.Active {
auth, err = nil, influxdb.ErrCredentialsUnauthorized
return
}
}()
switch c.Scheme {
case influxdb.SchemeV1Basic, influxdb.SchemeV1URL:
auth, err = v.tryV1Authorization(ctx, c)
if errors.Is(err, ErrAuthNotFound) {
return v.tryV2Authorization(ctx, c)
}
if err != nil {
return nil, v.normalizeError(err)
}
return
case influxdb.SchemeV1Token:
return v.tryV2Authorization(ctx, c)
default:
// this represents a programmer error
return nil, ErrUnsupportedScheme
}
}
func (v *Authorizer) tryV1Authorization(ctx context.Context, c influxdb.CredentialsV1) (auth *influxdb.Authorization, err error) {
auth, err = v.AuthV1.FindAuthorizationByToken(ctx, c.Username)
if err != nil {
return nil, err
}
if err := v.Comparer.ComparePassword(ctx, auth.ID, c.Token); err != nil {
return nil, err
}
return auth, nil
}
func (v *Authorizer) tryV2Authorization(ctx context.Context, c influxdb.CredentialsV1) (auth *influxdb.Authorization, err error) {
auth, err = v.AuthV2.FindAuthorizationByToken(ctx, c.Token)
if err != nil {
return nil, v.normalizeError(err)
}
return auth, nil
}
func (v *Authorizer) normalizeError(err error) error {
if err == nil {
return nil
}
var erri *influxdb.Error
if errors.As(err, &erri) {
switch erri.Code {
case influxdb.ENotFound, influxdb.EForbidden:
return influxdb.ErrCredentialsUnauthorized
}
}
return err
}

View File

@ -0,0 +1,339 @@
package authorization
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/influxdata/influxdb/v2"
itesting "github.com/influxdata/influxdb/v2/testing"
"github.com/influxdata/influxdb/v2/v1/authorization/mocks"
"github.com/stretchr/testify/assert"
)
func TestAuthorizer_Authorize(t *testing.T) {
var (
username = "foo"
token = "bar"
authID = itesting.MustIDBase16("0000000000001234")
userID = itesting.MustIDBase16("000000000000fefe")
expAuthErr = influxdb.ErrCredentialsUnauthorized.Error()
auth = &influxdb.Authorization{
ID: authID,
UserID: userID,
Token: username,
Status: influxdb.Active,
}
user = &influxdb.User{
ID: userID,
Status: influxdb.Active,
}
)
t.Run("invalid scheme returns error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
authz := Authorizer{}
cred := influxdb.CredentialsV1{
Scheme: "foo",
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, ErrUnsupportedScheme.Error())
})
tests := func(t *testing.T, scheme influxdb.SchemeV1) {
t.Run("invalid v1 and v2 token returns expected error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(nil, ErrAuthNotFound)
v2 := mocks.NewMockAuthTokenFinder(ctrl)
v2.EXPECT().
FindAuthorizationByToken(ctx, token).
Return(nil, ErrAuthNotFound)
authz := Authorizer{
AuthV1: v1,
AuthV2: v2,
}
cred := influxdb.CredentialsV1{
Scheme: scheme,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, expAuthErr)
})
t.Run("valid v1 token and invalid password returns expected error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(auth, nil)
pw := mocks.NewMockPasswordComparer(ctrl)
pw.EXPECT().
ComparePassword(ctx, authID, token).
Return(EIncorrectPassword)
authz := Authorizer{
AuthV1: v1,
Comparer: pw,
}
cred := influxdb.CredentialsV1{
Scheme: scheme,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, expAuthErr)
})
t.Run("valid v1 token and password returns authorization", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(auth, nil)
pw := mocks.NewMockPasswordComparer(ctrl)
pw.EXPECT().
ComparePassword(ctx, authID, token).
Return(nil)
uf := mocks.NewMockUserFinder(ctrl)
uf.EXPECT().
FindUserByID(ctx, userID).
Return(user, nil)
authz := Authorizer{
AuthV1: v1,
Comparer: pw,
User: uf,
}
cred := influxdb.CredentialsV1{
Scheme: scheme,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.NoError(t, gotErr)
assert.Equal(t, auth, gotAuth)
})
t.Run("invalid v1 token and valid v2 token returns authorization", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(nil, ErrAuthNotFound)
v2 := mocks.NewMockAuthTokenFinder(ctrl)
v2.EXPECT().
FindAuthorizationByToken(ctx, token).
Return(auth, nil)
uf := mocks.NewMockUserFinder(ctrl)
uf.EXPECT().
FindUserByID(ctx, userID).
Return(user, nil)
authz := Authorizer{
AuthV1: v1,
AuthV2: v2,
User: uf,
}
cred := influxdb.CredentialsV1{
Scheme: scheme,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.NoError(t, gotErr)
assert.Equal(t, auth, gotAuth)
})
}
t.Run("using Basic scheme", func(t *testing.T) {
tests(t, influxdb.SchemeV1Basic)
})
t.Run("using URL scheme", func(t *testing.T) {
tests(t, influxdb.SchemeV1URL)
})
t.Run("using Token scheme", func(t *testing.T) {
t.Run("invalid v2 token returns expected error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v2 := mocks.NewMockAuthTokenFinder(ctrl)
v2.EXPECT().
FindAuthorizationByToken(ctx, token).
Return(nil, ErrAuthNotFound)
authz := Authorizer{
AuthV2: v2,
}
cred := influxdb.CredentialsV1{
Scheme: influxdb.SchemeV1Token,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, expAuthErr)
})
t.Run("valid v2 token returns authorization", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v2 := mocks.NewMockAuthTokenFinder(ctrl)
v2.EXPECT().
FindAuthorizationByToken(ctx, token).
Return(auth, nil)
uf := mocks.NewMockUserFinder(ctrl)
uf.EXPECT().
FindUserByID(ctx, userID).
Return(user, nil)
authz := Authorizer{
AuthV2: v2,
User: uf,
}
cred := influxdb.CredentialsV1{
Scheme: influxdb.SchemeV1Token,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.NoError(t, gotErr)
assert.Equal(t, auth, gotAuth)
})
})
// test inactive user and inactive token
t.Run("inactive user returns error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(auth, nil)
pw := mocks.NewMockPasswordComparer(ctrl)
pw.EXPECT().
ComparePassword(ctx, authID, token).
Return(nil)
user := *user
user.Status = influxdb.Inactive
uf := mocks.NewMockUserFinder(ctrl)
uf.EXPECT().
FindUserByID(ctx, userID).
Return(&user, nil)
authz := Authorizer{
AuthV1: v1,
Comparer: pw,
User: uf,
}
cred := influxdb.CredentialsV1{
Scheme: influxdb.SchemeV1Basic,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, expAuthErr)
})
t.Run("inactive token returns error", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
ctx := context.Background()
auth := *auth
auth.Status = influxdb.Inactive
v1 := mocks.NewMockAuthTokenFinder(ctrl)
v1.EXPECT().
FindAuthorizationByToken(ctx, username).
Return(&auth, nil)
pw := mocks.NewMockPasswordComparer(ctrl)
pw.EXPECT().
ComparePassword(ctx, authID, token).
Return(nil)
authz := Authorizer{
AuthV1: v1,
Comparer: pw,
}
cred := influxdb.CredentialsV1{
Scheme: influxdb.SchemeV1Basic,
Username: username,
Token: token,
}
gotAuth, gotErr := authz.Authorize(ctx, cred)
assert.Nil(t, gotAuth)
assert.EqualError(t, gotErr, expAuthErr)
})
}

View File

@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/influxdata/influxdb/v2/v1/authorization (interfaces: AuthTokenFinder)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
gomock "github.com/golang/mock/gomock"
influxdb "github.com/influxdata/influxdb/v2"
reflect "reflect"
)
// MockAuthTokenFinder is a mock of AuthTokenFinder interface
type MockAuthTokenFinder struct {
ctrl *gomock.Controller
recorder *MockAuthTokenFinderMockRecorder
}
// MockAuthTokenFinderMockRecorder is the mock recorder for MockAuthTokenFinder
type MockAuthTokenFinderMockRecorder struct {
mock *MockAuthTokenFinder
}
// NewMockAuthTokenFinder creates a new mock instance
func NewMockAuthTokenFinder(ctrl *gomock.Controller) *MockAuthTokenFinder {
mock := &MockAuthTokenFinder{ctrl: ctrl}
mock.recorder = &MockAuthTokenFinderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthTokenFinder) EXPECT() *MockAuthTokenFinderMockRecorder {
return m.recorder
}
// FindAuthorizationByToken mocks base method
func (m *MockAuthTokenFinder) FindAuthorizationByToken(arg0 context.Context, arg1 string) (*influxdb.Authorization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindAuthorizationByToken", arg0, arg1)
ret0, _ := ret[0].(*influxdb.Authorization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindAuthorizationByToken indicates an expected call of FindAuthorizationByToken
func (mr *MockAuthTokenFinderMockRecorder) FindAuthorizationByToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAuthorizationByToken", reflect.TypeOf((*MockAuthTokenFinder)(nil).FindAuthorizationByToken), arg0, arg1)
}

View File

@ -0,0 +1,49 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/influxdata/influxdb/v2/v1/authorization (interfaces: PasswordComparer)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
gomock "github.com/golang/mock/gomock"
influxdb "github.com/influxdata/influxdb/v2"
reflect "reflect"
)
// MockPasswordComparer is a mock of PasswordComparer interface
type MockPasswordComparer struct {
ctrl *gomock.Controller
recorder *MockPasswordComparerMockRecorder
}
// MockPasswordComparerMockRecorder is the mock recorder for MockPasswordComparer
type MockPasswordComparerMockRecorder struct {
mock *MockPasswordComparer
}
// NewMockPasswordComparer creates a new mock instance
func NewMockPasswordComparer(ctrl *gomock.Controller) *MockPasswordComparer {
mock := &MockPasswordComparer{ctrl: ctrl}
mock.recorder = &MockPasswordComparerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPasswordComparer) EXPECT() *MockPasswordComparerMockRecorder {
return m.recorder
}
// ComparePassword mocks base method
func (m *MockPasswordComparer) ComparePassword(arg0 context.Context, arg1 influxdb.ID, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ComparePassword", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// ComparePassword indicates an expected call of ComparePassword
func (mr *MockPasswordComparerMockRecorder) ComparePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComparePassword", reflect.TypeOf((*MockPasswordComparer)(nil).ComparePassword), arg0, arg1, arg2)
}

View File

@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/influxdata/influxdb/v2/v1/authorization (interfaces: UserFinder)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
gomock "github.com/golang/mock/gomock"
influxdb "github.com/influxdata/influxdb/v2"
reflect "reflect"
)
// MockUserFinder is a mock of UserFinder interface
type MockUserFinder struct {
ctrl *gomock.Controller
recorder *MockUserFinderMockRecorder
}
// MockUserFinderMockRecorder is the mock recorder for MockUserFinder
type MockUserFinderMockRecorder struct {
mock *MockUserFinder
}
// NewMockUserFinder creates a new mock instance
func NewMockUserFinder(ctrl *gomock.Controller) *MockUserFinder {
mock := &MockUserFinder{ctrl: ctrl}
mock.recorder = &MockUserFinderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUserFinder) EXPECT() *MockUserFinderMockRecorder {
return m.recorder
}
// FindUserByID mocks base method
func (m *MockUserFinder) FindUserByID(arg0 context.Context, arg1 influxdb.ID) (*influxdb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindUserByID", arg0, arg1)
ret0, _ := ret[0].(*influxdb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindUserByID indicates an expected call of FindUserByID
func (mr *MockUserFinderMockRecorder) FindUserByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUserByID", reflect.TypeOf((*MockUserFinder)(nil).FindUserByID), arg0, arg1)
}