Merge pull request #2435 from influxdata/multitenancy_prevent_bad_add_org_user

Prevent unintended update of existing user on Add; fix duplicate User & Org creation client UX
pull/10616/head
Jared Scheib 2017-12-04 14:15:27 -08:00 committed by GitHub
commit adce9f159c
7 changed files with 98 additions and 15 deletions

View File

@ -87,7 +87,7 @@ func (s *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronogr
// Add creates a new Organization in the OrganizationsStore // Add creates a new Organization in the OrganizationsStore
func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) { func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
if !s.nameIsUnique(ctx, o.Name) { if !s.nameIsUnique(ctx, o.Name) {
return nil, chronograf.ErrOrganizationNameTaken return nil, chronograf.ErrOrganizationAlreadyExists
} }
if err := s.client.db.Update(func(tx *bolt.Tx) error { if err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(OrganizationsBucket) b := tx.Bucket(OrganizationsBucket)
@ -276,7 +276,7 @@ func (s *OrganizationsStore) Update(ctx context.Context, o *chronograf.Organizat
return err return err
} }
if o.Name != org.Name && !s.nameIsUnique(ctx, o.Name) { if o.Name != org.Name && !s.nameIsUnique(ctx, o.Name) {
return chronograf.ErrOrganizationNameTaken return chronograf.ErrOrganizationAlreadyExists
} }
return s.client.db.Update(func(tx *bolt.Tx) error { return s.client.db.Update(func(tx *bolt.Tx) error {
if v, err := internal.MarshalOrganization(o); err != nil { if v, err := internal.MarshalOrganization(o); err != nil {

View File

@ -373,7 +373,7 @@ func TestOrganizationsStore_Update(t *testing.T) {
addFirst: true, addFirst: true,
}, },
{ {
name: "Update organization name - name already taken", name: "Update organization name - organization already exists",
fields: fields{ fields: fields{
orgs: []chronograf.Organization{ orgs: []chronograf.Organization{
{ {
@ -552,7 +552,7 @@ func TestOrganizationsStore_Add(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
name: "Add organization - name already taken", name: "Add organization - organization already exists",
fields: fields{ fields: fields{
orgs: []chronograf.Organization{ orgs: []chronograf.Organization{
{ {

View File

@ -33,7 +33,7 @@ const (
ErrAuthentication = Error("user not authenticated") ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method") ErrUninitialized = Error("client uninitialized. Call Open() method")
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
ErrOrganizationNameTaken = Error("organization name is taken") ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization") ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
) )

View File

@ -128,6 +128,16 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
} }
} }
// If the user already has a role in the organization then the user
// cannot be "created".
// This can be thought of as:
// (total # of roles a user has) - (# of roles not in the organization) = (# of roles in organization)
// if this value is greater than 1 the user cannot be "added".
numRolesInOrganization := len(usr.Roles) - len(roles)
if numRolesInOrganization > 0 {
return nil, chronograf.ErrUserAlreadyExists
}
// Set the users roles to be the union of the roles set on the provided user // Set the users roles to be the union of the roles set on the provided user
// and the user that was found in the underlying store // and the user that was found in the underlying store
usr.Roles = append(roles, u.Roles...) usr.Roles = append(roles, u.Roles...)

View File

@ -325,6 +325,50 @@ func TestUsersStore_Add(t *testing.T) {
}, },
}, },
}, },
{
name: "Add user that already exists",
fields: fields{
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
return &chronograf.User{
ID: 1234,
Name: "docbrown",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Organization: "1337",
Name: "editor",
},
},
}, nil
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
ID: 1234,
Name: "docbrown",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Organization: "1337",
Name: "admin",
},
},
},
orgID: "1337",
},
wantErr: true,
},
{ {
name: "Has invalid Role: missing Organization", name: "Has invalid Role: missing Organization",
fields: fields{ fields: fields{

View File

@ -1,3 +1,6 @@
import _ from 'lodash'
import uuid from 'node-uuid'
import { import {
getUsers as getUsersAJAX, getUsers as getUsersAJAX,
getOrganizations as getOrganizationsAJAX, getOrganizations as getOrganizationsAJAX,
@ -12,6 +15,8 @@ import {
import {publishAutoDismissingNotification} from 'shared/dispatchers' import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {errorThrown} from 'shared/actions/errors' import {errorThrown} from 'shared/actions/errors'
import {REVERT_STATE_DELAY} from 'shared/constants'
// action creators // action creators
// response contains `users` and `links` // response contains `users` and `links`
@ -109,13 +114,20 @@ export const loadOrganizationsAsync = url => async dispatch => {
} }
export const createUserAsync = (url, user) => async dispatch => { export const createUserAsync = (url, user) => async dispatch => {
dispatch(addUser(user)) // temp uuid is added to be able to disambiguate a created user that has the
// same scheme, provider, and name as an existing user
const userWithTempID = {...user, _tempID: uuid.v4()}
dispatch(addUser(userWithTempID))
try { try {
const {data} = await createUserAJAX(url, user) const {data} = await createUserAJAX(url, user)
dispatch(syncUser(user, data)) dispatch(syncUser(userWithTempID, data))
} catch (error) { } catch (error) {
dispatch(errorThrown(error)) const message = `${_.upperFirst(
dispatch(removeUser(user)) _.toLower(error.data.message)
)}: ${user.scheme}::${user.provider}::${user.name}`
dispatch(errorThrown(error, message))
// undo optimistic update
setTimeout(() => dispatch(removeUser(userWithTempID)), REVERT_STATE_DELAY)
} }
} }
@ -168,13 +180,23 @@ export const createOrganizationAsync = (
url, url,
organization organization
) => async dispatch => { ) => async dispatch => {
dispatch(addOrganization(organization)) // temp uuid is added to be able to disambiguate a created organization with
// the same name as an existing organization
const organizationWithTempID = {...organization, _tempID: uuid.v4()}
dispatch(addOrganization(organizationWithTempID))
try { try {
const {data} = await createOrganizationAJAX(url, organization) const {data} = await createOrganizationAJAX(url, organization)
dispatch(syncOrganization(organization, data)) dispatch(syncOrganization(organization, data))
} catch (error) { } catch (error) {
dispatch(errorThrown(error)) const message = `${_.upperFirst(
dispatch(removeOrganization(organization)) _.toLower(error.data.message)
)}: ${organization.name}`
dispatch(errorThrown(error, message))
// undo optimistic update
setTimeout(
() => dispatch(removeOrganization(organizationWithTempID)),
REVERT_STATE_DELAY
)
} }
} }

View File

@ -44,8 +44,12 @@ const adminChronograf = (state = initialState, action) => {
const {user} = action.payload const {user} = action.payload
return { return {
...state, ...state,
// stale user does not have links, so uniqueness is on name, provider, & scheme // stale user does not necessarily have links, so uniqueness is on name,
users: state.users.filter(u => !isSameUser(u, user)), // provider, & scheme, except for a created users that is a duplicate
// of an existing user, in which case a temp uuid is used to match
users: state.users.filter(
u => (user._tempID ? u._tempID !== user._tempID : u.id !== user.id)
),
} }
} }
@ -80,7 +84,10 @@ const adminChronograf = (state = initialState, action) => {
return { return {
...state, ...state,
organizations: state.organizations.filter( organizations: state.organizations.filter(
o => o.name !== organization.name o =>
organization._tempID
? o._tempID !== organization._tempID
: o.id !== organization.id
), ),
} }
} }