feat(authorization): Add bcrypt password support to v1 authorizations

This commit extends the `v1/authorization` package to support
passwords associated with a token.

The summary of changes include:

* authorization.Service implements influxdb.PasswordsService
* Setting passwords for authorizations
* Verifying (comparing) passwords for a given authorization
* A service to cache comparing passwords, using a weaker hash
  that will live in memory only. This implementation is copied
  from InfluxDB 1.x
* Extended HTTP service to set a password using
  /private/legacy/authorizations/{id}/password

Closes #
pull/19829/head
Stuart Carnie 2020-10-27 17:30:34 -07:00
parent 82903c4922
commit 6bc4158a46
18 changed files with 879 additions and 26 deletions

View File

@ -1284,15 +1284,12 @@ func (m *Launcher) run(ctx context.Context) (err error) {
var v1AuthHTTPServer *authv1.AuthHandler
{
var v1AuthSvc platform.AuthorizationService
{
authStore, err := authv1.NewStore(m.kvStore)
if err != nil {
m.log.Error("Failed creating new authorization store", zap.Error(err))
return err
}
v1AuthSvc = authv1.NewService(authStore, ts)
authStore, err := authv1.NewStore(m.kvStore)
if err != nil {
m.log.Error("Failed creating new authorization store", zap.Error(err))
return err
}
v1AuthSvc := authv1.NewService(authStore, ts)
authLogger := m.log.With(zap.String("handler", "v1_authorization"))
@ -1300,7 +1297,9 @@ func (m *Launcher) run(ctx context.Context) (err error) {
authService = authorization.NewAuthedAuthorizationService(v1AuthSvc, ts)
authService = authorization.NewAuthLogger(authLogger, authService)
v1AuthHTTPServer = authv1.NewHTTPAuthHandler(m.log, authService, ts)
passService := authv1.NewAuthedPasswordService(authv1.AuthFinder(v1AuthSvc), authv1.PasswordService(v1AuthSvc))
v1AuthHTTPServer = authv1.NewHTTPAuthHandler(m.log, authService, passService, ts)
}
var sessionHTTPServer *session.SessionHandler

2
go.mod
View File

@ -32,7 +32,7 @@ require (
github.com/go-stack/stack v1.8.0
github.com/gogo/protobuf v1.3.1
github.com/golang/gddo v0.0.0-20181116215533-9bd4a3295021
github.com/golang/mock v1.3.1
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.3.3
github.com/golang/snappy v0.0.1
github.com/google/btree v1.0.0

3
go.sum
View File

@ -219,8 +219,9 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18h
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

View File

@ -0,0 +1,7 @@
package all
import "github.com/influxdata/influxdb/v2/kv/migration"
var Migration0009_LegacyAuthPasswordBuckets = migration.CreateBuckets(
"Create legacy auth password bucket",
[]byte("legacy/authorizationPasswordv1"))

View File

@ -23,5 +23,7 @@ var Migrations = [...]migration.Spec{
Migration0007_CreateMetaDataBucket,
// LegacyAuthBuckets
Migration0008_LegacyAuthBuckets,
// LegacyAuthPasswordBuckets
Migration0009_LegacyAuthPasswordBuckets,
// {{ do_not_edit . }}
}

77
mock/passwords_service.go Normal file
View File

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

View File

@ -0,0 +1,110 @@
package authorization
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/sha256"
"io"
"sync"
"github.com/influxdata/influxdb/v2"
)
// An implementation of influxdb.PasswordsService that will perform
// ComparePassword requests at a reduced cost under certain
// conditions. See ComparePassword for further information.
//
// The cache is only valid for the duration of the process.
type CachingPasswordsService struct {
inner influxdb.PasswordsService
mu sync.RWMutex // protects concurrent access to authCache
authCache map[influxdb.ID]authUser
}
func NewCachingPasswordsService(inner influxdb.PasswordsService) *CachingPasswordsService {
return &CachingPasswordsService{inner: inner, authCache: make(map[influxdb.ID]authUser)}
}
var _ influxdb.PasswordsService = (*CachingPasswordsService)(nil)
func (c *CachingPasswordsService) SetPassword(ctx context.Context, id influxdb.ID, password string) error {
err := c.inner.SetPassword(ctx, id, password)
if err == nil {
c.mu.Lock()
delete(c.authCache, id)
c.mu.Unlock()
}
return err
}
// ComparePassword will attempt to perform the comparison using a lower cost hashing function
// if influxdb.ContextHasPasswordCacheOption returns true for ctx.
func (c *CachingPasswordsService) ComparePassword(ctx context.Context, id influxdb.ID, password string) error {
c.mu.RLock()
au, ok := c.authCache[id]
c.mu.RUnlock()
if ok {
// verify the password using the cached salt and hash
if bytes.Equal(c.hashWithSalt(au.salt, password), au.hash) {
return nil
}
// fall through to requiring a full bcrypt hash for invalid passwords
}
err := c.inner.ComparePassword(ctx, id, password)
if err != nil {
return err
}
if salt, hashed, err := c.saltedHash(password); err == nil {
c.mu.Lock()
c.authCache[id] = authUser{salt: salt, hash: hashed}
c.mu.Unlock()
}
return nil
}
func (c *CachingPasswordsService) CompareAndSetPassword(ctx context.Context, id influxdb.ID, old, new string) error {
err := c.inner.CompareAndSetPassword(ctx, id, old, new)
if err == nil {
c.mu.Lock()
delete(c.authCache, id)
c.mu.Unlock()
}
return err
}
// NOTE(sgc): This caching implementation was lifted from the 1.x source
// https://github.com/influxdata/influxdb/blob/c1e11e732e145fc1a356535ddf3dcb9fb732a22b/services/meta/client.go#L390-L406
const (
// SaltBytes is the number of bytes used for salts.
SaltBytes = 32
)
type authUser struct {
salt []byte
hash []byte
}
// hashWithSalt returns a salted hash of password using salt.
func (c *CachingPasswordsService) hashWithSalt(salt []byte, password string) []byte {
hasher := sha256.New()
hasher.Write(salt)
hasher.Write([]byte(password))
return hasher.Sum(nil)
}
// saltedHash returns a salt and salted hash of password.
func (c *CachingPasswordsService) saltedHash(password string) (salt, hash []byte, err error) {
salt = make([]byte, SaltBytes)
if _, err := io.ReadFull(crand.Reader, salt); err != nil {
return nil, nil, err
}
return salt, c.hashWithSalt(salt, password), nil
}

View File

@ -0,0 +1,183 @@
package authorization
import (
"context"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/stretchr/testify/assert"
)
func TestCachingPasswordsService(t *testing.T) {
const (
user1 = influxdb.ID(1)
user2 = influxdb.ID(2)
)
makeUser := func(salt, pass string) authUser {
if len(salt) != SaltBytes {
panic("invalid salt")
}
var ps CachingPasswordsService
return authUser{salt: []byte(salt), hash: ps.hashWithSalt([]byte(salt), pass)}
}
var (
userE1 = makeUser(strings.Repeat("salt---1", 4), "foo")
userE2 = makeUser(strings.Repeat("salt---2", 4), "bar")
)
t.Run("SetPassword deletes cached user", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
SetPassword(gomock.Any(), user1, "foo").
Return(nil)
s := NewCachingPasswordsService(inner)
s.authCache[user1] = userE1
s.authCache[user2] = userE2
ctx := context.Background()
_, ok := s.authCache[user1]
assert.True(t, ok)
assert.NoError(t, s.SetPassword(ctx, user1, "foo"))
_, ok = s.authCache[user1]
assert.False(t, ok)
_, ok = s.authCache[user2]
assert.True(t, ok)
})
t.Run("ComparePassword adds cached user", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
ComparePassword(gomock.Any(), user1, "foo").
Return(nil)
s := NewCachingPasswordsService(inner)
s.authCache[user2] = userE2
ctx := context.Background()
assert.NoError(t, s.ComparePassword(ctx, user1, "foo"))
_, ok := s.authCache[user1]
assert.True(t, ok)
_, ok = s.authCache[user2]
assert.True(t, ok)
})
t.Run("ComparePassword does not add cached user when inner errors", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
ComparePassword(gomock.Any(), user1, "foo").
Return(tenant.EShortPassword)
s := NewCachingPasswordsService(inner)
s.authCache[user2] = userE2
ctx := context.Background()
assert.Error(t, s.ComparePassword(ctx, user1, "foo"))
_, ok := s.authCache[user1]
assert.False(t, ok)
_, ok = s.authCache[user2]
assert.True(t, ok)
})
t.Run("ComparePassword uses cached password when context option set", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
s := NewCachingPasswordsService(inner)
s.authCache[user1] = userE1
s.authCache[user2] = userE2
ctx := context.Background()
assert.NoError(t, s.ComparePassword(ctx, user1, "foo"))
})
t.Run("CompareAndSetPassword deletes cached user", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
CompareAndSetPassword(gomock.Any(), user1, "foo", "foo2").
Return(nil)
s := NewCachingPasswordsService(inner)
s.authCache[user1] = userE1
s.authCache[user2] = userE2
ctx := context.Background()
assert.NoError(t, s.CompareAndSetPassword(ctx, user1, "foo", "foo2"))
_, ok := s.authCache[user1]
assert.False(t, ok)
_, ok = s.authCache[user2]
assert.True(t, ok)
})
// The following tests ensure the service does not change state for invalid
// requests, which may permit a certain class of attacks.
t.Run("SetPassword does not delete cached user when inner errors", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
SetPassword(gomock.Any(), user1, "foo").
Return(tenant.EShortPassword)
s := NewCachingPasswordsService(inner)
s.authCache[user1] = userE1
s.authCache[user2] = userE2
ctx := context.Background()
_, ok := s.authCache[user1]
assert.True(t, ok)
assert.EqualError(t, s.SetPassword(ctx, user1, "foo"), tenant.EShortPassword.Error())
_, ok = s.authCache[user1]
assert.True(t, ok)
})
t.Run("CompareAndSetPassword does not delete cached user when inner errors", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
inner := mock.NewMockPasswordsService(ctrl)
inner.EXPECT().
CompareAndSetPassword(gomock.Any(), user1, "foo", "foo2").
Return(tenant.EShortPassword)
s := NewCachingPasswordsService(inner)
s.authCache[user1] = userE1
s.authCache[user2] = userE2
ctx := context.Background()
assert.Error(t, s.CompareAndSetPassword(ctx, user1, "foo", "foo2"))
_, ok := s.authCache[user1]
assert.True(t, ok)
_, ok = s.authCache[user2]
assert.True(t, ok)
})
}

View File

@ -8,7 +8,10 @@ import (
"github.com/influxdata/influxdb/v2/pkg/httpc"
)
var _ influxdb.AuthorizationService = (*Client)(nil)
var (
_ influxdb.AuthorizationService = (*Client)(nil)
_ PasswordService = (*Client)(nil)
)
// Client connects to Influx via HTTP using tokens to manage authorizations
type Client struct {
@ -104,3 +107,12 @@ func (s *Client) DeleteAuthorization(ctx context.Context, id influxdb.ID) error
Delete(prefixAuthorization, id.String()).
Do(ctx)
}
// SetPassword sets the password for the authorization token id.
func (s *Client) SetPassword(ctx context.Context, id influxdb.ID, password string) error {
return s.Client.
PostJSON(passwordSetRequest{
Password: password,
}, prefixAuthorization, id.String(), "password").
Do(ctx)
}

View File

@ -27,20 +27,26 @@ type TenantService interface {
FindBucketByID(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error)
}
type PasswordService interface {
SetPassword(ctx context.Context, id influxdb.ID, password string) error
}
type AuthHandler struct {
chi.Router
api *kithttp.API
log *zap.Logger
authSvc influxdb.AuthorizationService
passwordSvc PasswordService
tenantService TenantService
}
// NewHTTPAuthHandler constructs a new http server.
func NewHTTPAuthHandler(log *zap.Logger, authService influxdb.AuthorizationService, tenantService TenantService) *AuthHandler {
func NewHTTPAuthHandler(log *zap.Logger, authService influxdb.AuthorizationService, passwordService PasswordService, tenantService TenantService) *AuthHandler {
h := &AuthHandler{
api: kithttp.NewAPI(kithttp.WithLog(log)),
log: log,
authSvc: authService,
passwordSvc: passwordService,
tenantService: tenantService,
}
@ -59,6 +65,7 @@ func NewHTTPAuthHandler(log *zap.Logger, authService influxdb.AuthorizationServi
r.Get("/", h.handleGetAuthorization)
r.Patch("/", h.handleUpdateAuthorization)
r.Delete("/", h.handleDeleteAuthorization)
r.Post("/password", h.handlePostUserPassword)
})
})
@ -623,3 +630,39 @@ func (h *AuthHandler) handleDeleteAuthorization(w http.ResponseWriter, r *http.R
w.WriteHeader(http.StatusNoContent)
}
// password APIs
type passwordSetRequest struct {
Password string `json:"password"`
}
// handlePutPassword is the HTTP handler for the PUT /private/legacy/authorizations/:id/password
func (h *AuthHandler) handlePostUserPassword(w http.ResponseWriter, r *http.Request) {
var body passwordSetRequest
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
h.api.Err(w, r, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
})
return
}
param := chi.URLParam(r, "id")
authID, err := influxdb.IDFromString(param)
if err != nil {
h.api.Err(w, r, &influxdb.Error{
Msg: "invalid authorization ID provided in route",
})
return
}
err = h.passwordSvc.SetPassword(r.Context(), *authID, body.Password)
if err != nil {
h.api.Err(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -242,7 +242,7 @@ func TestService_handlePostAuthorization(t *testing.T) {
svc := NewService(storage, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), svc, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), svc, nil, tt.fields.TenantService)
router := chi.NewRouter()
router.Mount(handler.Prefix(), handler)
@ -447,7 +447,7 @@ func TestService_handleGetAuthorization(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Helper()
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), tt.fields.AuthorizationService, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), tt.fields.AuthorizationService, nil, tt.fields.TenantService)
router := chi.NewRouter()
router.Mount(handler.Prefix(), handler)
@ -786,7 +786,7 @@ func TestService_handleGetAuthorizations(t *testing.T) {
svc := NewService(storage, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), svc, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), svc, nil, tt.fields.TenantService)
router := chi.NewRouter()
router.Mount(handler.Prefix(), handler)
@ -892,7 +892,7 @@ func TestService_handleDeleteAuthorization(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Helper()
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), tt.fields.AuthorizationService, tt.fields.TenantService)
handler := NewHTTPAuthHandler(zaptest.NewLogger(t), tt.fields.AuthorizationService, nil, tt.fields.TenantService)
router := chi.NewRouter()
router.Mount(handler.Prefix(), handler)

View File

@ -0,0 +1,37 @@
package authorization
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
)
type AuthFinder interface {
FindAuthorizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Authorization, error)
}
// AuthedPasswordService is middleware for authorizing requests to the inner PasswordService.
type AuthedPasswordService struct {
auth AuthFinder
inner PasswordService
}
// NewAuthedPasswordService wraps an existing PasswordService with authorization middleware.
func NewAuthedPasswordService(auth AuthFinder, inner PasswordService) *AuthedPasswordService {
return &AuthedPasswordService{auth: auth, inner: inner}
}
// SetPassword overrides the password of a known user.
func (s *AuthedPasswordService) SetPassword(ctx context.Context, authID influxdb.ID, password string) error {
auth, err := s.auth.FindAuthorizationByID(ctx, authID)
if err != nil {
return ErrAuthNotFound
}
if _, _, err := authorizer.AuthorizeWriteResource(ctx, influxdb.UsersResourceType, auth.UserID); err != nil {
return err
}
return s.inner.SetPassword(ctx, authID, password)
}

View File

@ -0,0 +1,110 @@
package authorization_test
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
itest "github.com/influxdata/influxdb/v2/testing"
"github.com/influxdata/influxdb/v2/v1/authorization"
"github.com/influxdata/influxdb/v2/v1/authorization/mocks"
"github.com/stretchr/testify/assert"
)
func TestAuthedPasswordService_SetPassword(t *testing.T) {
var (
authID = itest.MustIDBase16("0000000000001000")
userID = itest.MustIDBase16("0000000000002000")
orgID = itest.MustIDBase16("0000000000003000")
)
t.Run("error when auth not found", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
af := mocks.NewMockAuthFinder(ctrl)
af.EXPECT().
FindAuthorizationByID(ctx, authID).
Return(nil, &influxdb.Error{})
ps := authorization.NewAuthedPasswordService(af, nil)
err := ps.SetPassword(ctx, authID, "foo")
assert.EqualError(t, err, authorization.ErrAuthNotFound.Error())
})
t.Run("error when no authorizer", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
auth := influxdb.Authorization{
ID: authID,
OrgID: orgID,
UserID: userID,
}
af := mocks.NewMockAuthFinder(ctrl)
af.EXPECT().
FindAuthorizationByID(ctx, authID).
Return(&auth, nil)
ps := authorization.NewAuthedPasswordService(af, nil)
err := ps.SetPassword(ctx, authID, "foo")
assert.EqualError(t, err, "authorizer not found on context")
})
t.Run("error with restricted permission", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
influxdb.OperPermissions()
auth := influxdb.Authorization{
ID: authID,
OrgID: orgID,
UserID: userID,
Status: influxdb.Active,
}
ctx := context.Background()
ctx = icontext.SetAuthorizer(ctx, &auth)
af := mocks.NewMockAuthFinder(ctrl)
af.EXPECT().
FindAuthorizationByID(ctx, authID).
Return(&auth, nil)
ps := authorization.NewAuthedPasswordService(af, nil)
err := ps.SetPassword(ctx, authID, "foo")
assert.Error(t, err)
})
t.Run("success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
influxdb.OperPermissions()
auth := influxdb.Authorization{
ID: authID,
OrgID: orgID,
UserID: userID,
Status: influxdb.Active,
Permissions: influxdb.MePermissions(userID),
}
ctx := context.Background()
ctx = icontext.SetAuthorizer(ctx, &auth)
af := mocks.NewMockAuthFinder(ctrl)
af.EXPECT().
FindAuthorizationByID(ctx, authID).
Return(&auth, nil)
inner := mocks.NewMockPasswordService(ctrl)
inner.EXPECT().
SetPassword(ctx, authID, "foo").
Return(nil)
ps := authorization.NewAuthedPasswordService(af, inner)
err := ps.SetPassword(ctx, authID, "foo")
assert.NoError(t, err)
})
}

View File

@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/influxdata/influxdb/v2/v1/authorization (interfaces: AuthFinder)
// 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"
)
// MockAuthFinder is a mock of AuthFinder interface
type MockAuthFinder struct {
ctrl *gomock.Controller
recorder *MockAuthFinderMockRecorder
}
// MockAuthFinderMockRecorder is the mock recorder for MockAuthFinder
type MockAuthFinderMockRecorder struct {
mock *MockAuthFinder
}
// NewMockAuthFinder creates a new mock instance
func NewMockAuthFinder(ctrl *gomock.Controller) *MockAuthFinder {
mock := &MockAuthFinder{ctrl: ctrl}
mock.recorder = &MockAuthFinderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthFinder) EXPECT() *MockAuthFinderMockRecorder {
return m.recorder
}
// FindAuthorizationByID mocks base method
func (m *MockAuthFinder) FindAuthorizationByID(arg0 context.Context, arg1 influxdb.ID) (*influxdb.Authorization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindAuthorizationByID", arg0, arg1)
ret0, _ := ret[0].(*influxdb.Authorization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindAuthorizationByID indicates an expected call of FindAuthorizationByID
func (mr *MockAuthFinderMockRecorder) FindAuthorizationByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAuthorizationByID", reflect.TypeOf((*MockAuthFinder)(nil).FindAuthorizationByID), arg0, arg1)
}

View File

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

View File

@ -6,22 +6,22 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/rand"
)
var _ influxdb.AuthorizationService = (*Service)(nil)
var (
_ influxdb.AuthorizationService = (*Service)(nil)
_ influxdb.PasswordsService = (*Service)(nil)
)
type Service struct {
store *Store
tokenGenerator influxdb.TokenGenerator
tenantService TenantService
store *Store
tenantService TenantService
}
func NewService(st *Store, ts TenantService) influxdb.AuthorizationService {
func NewService(st *Store, ts TenantService) *Service {
return &Service{
store: st,
tokenGenerator: rand.NewTokenGenerator(64),
tenantService: ts,
store: st,
tenantService: ts,
}
}

View File

@ -0,0 +1,105 @@
package authorization
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/tenant"
"golang.org/x/crypto/bcrypt"
)
var EIncorrectPassword = tenant.EIncorrectPassword
// SetPasswordHash updates the password hash for id. If passHash is not a valid bcrypt hash,
// SetPasswordHash returns an error.
//
// This API is intended for upgrading 1.x users.
func (s *Service) SetPasswordHash(ctx context.Context, authID influxdb.ID, passHash string) error {
// verify passHash is a valid bcrypt hash
_, err := bcrypt.Cost([]byte(passHash))
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid bcrypt hash",
Err: err,
}
}
// set password
return s.store.Update(ctx, func(tx kv.Tx) error {
_, err := s.store.GetAuthorizationByID(ctx, tx, authID)
if err != nil {
return ErrAuthNotFound
}
return s.store.SetPassword(ctx, tx, authID, passHash)
})
}
// SetPassword overrides the password of a known user.
func (s *Service) SetPassword(ctx context.Context, authID influxdb.ID, password string) error {
if len(password) < 8 {
return tenant.EShortPassword
}
passHash, err := encryptPassword(password)
if err != nil {
return err
}
// set password
return s.store.Update(ctx, func(tx kv.Tx) error {
_, err := s.store.GetAuthorizationByID(ctx, tx, authID)
if err != nil {
return ErrAuthNotFound
}
return s.store.SetPassword(ctx, tx, authID, passHash)
})
}
// ComparePassword checks if the password matches the password recorded.
// Passwords that do not match return errors.
func (s *Service) ComparePassword(ctx context.Context, authID influxdb.ID, password string) error {
// get password
var hash []byte
err := s.store.View(ctx, func(tx kv.Tx) error {
_, err := s.store.GetAuthorizationByID(ctx, tx, authID)
if err != nil {
return ErrAuthNotFound
}
h, err := s.store.GetPassword(ctx, tx, authID)
if err != nil {
if err == kv.ErrKeyNotFound {
return EIncorrectPassword
}
return err
}
hash = []byte(h)
return nil
})
if err != nil {
return err
}
// compare password
if err := bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil {
return EIncorrectPassword
}
return nil
}
// CompareAndSetPassword checks the password and if they match
// updates to the new password.
func (s *Service) CompareAndSetPassword(ctx context.Context, authID influxdb.ID, old, new string) error {
err := s.ComparePassword(ctx, authID, old)
if err != nil {
return err
}
return s.SetPassword(ctx, authID, new)
}
func encryptPassword(password string) (string, error) {
passHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(passHash), nil
}

View File

@ -0,0 +1,68 @@
package authorization
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
)
var (
passwordBucket = []byte("legacy/authorizationPasswordv1")
)
// UnavailablePasswordServiceError is used if we aren't able to add the
// password to the store, it means the store is not available at the moment
// (e.g. network).
func UnavailablePasswordServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "unable to access password bucket",
Err: err,
}
}
func (s *Store) GetPassword(ctx context.Context, tx kv.Tx, id influxdb.ID) (string, error) {
encodedID, err := id.Encode()
if err != nil {
return "", ErrInvalidAuthIDError(err)
}
b, err := tx.Bucket(passwordBucket)
if err != nil {
return "", UnavailablePasswordServiceError(err)
}
passwd, err := b.Get(encodedID)
return string(passwd), err
}
func (s *Store) SetPassword(ctx context.Context, tx kv.Tx, id influxdb.ID, password string) error {
encodedID, err := id.Encode()
if err != nil {
return ErrInvalidAuthIDError(err)
}
b, err := tx.Bucket(passwordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
return b.Put(encodedID, []byte(password))
}
func (s *Store) DeletePassword(ctx context.Context, tx kv.Tx, id influxdb.ID) error {
encodedID, err := id.Encode()
if err != nil {
return ErrInvalidAuthIDError(err)
}
b, err := tx.Bucket(passwordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
return b.Delete(encodedID)
}