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
parent
82903c4922
commit
6bc4158a46
|
@ -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
2
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||
|
|
|
@ -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"))
|
|
@ -23,5 +23,7 @@ var Migrations = [...]migration.Spec{
|
|||
Migration0007_CreateMetaDataBucket,
|
||||
// LegacyAuthBuckets
|
||||
Migration0008_LegacyAuthBuckets,
|
||||
// LegacyAuthPasswordBuckets
|
||||
Migration0009_LegacyAuthPasswordBuckets,
|
||||
// {{ do_not_edit . }}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
Loading…
Reference in New Issue