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 UXpull/10616/head
commit
adce9f159c
|
@ -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 {
|
||||||
|
|
|
@ -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{
|
||||||
{
|
{
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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...)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue