Merge branch 'multitenancy' into multitenancy_whitelist_only

pull/10616/head
Luke Morris 2017-11-10 17:52:06 -08:00
commit 5a2befccff
16 changed files with 157 additions and 93 deletions

View File

@ -195,11 +195,15 @@ func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
}
}
// Make a copy of the usr so that we dont modify the underlying add roles on to
// the user that was passed in
user := *usr
// 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
u.Roles = append(roles, usr.Roles...)
user.Roles = append(roles, usr.Roles...)
return s.store.Update(ctx, u)
return s.store.Update(ctx, &user)
}
// All returns all users where roles have been filters to be exclusively for

View File

@ -561,10 +561,11 @@ func TestUsersStore_Update(t *testing.T) {
UsersStore chronograf.UsersStore
}
type args struct {
ctx context.Context
usr *chronograf.User
roles []chronograf.Role
orgID string
ctx context.Context
usr *chronograf.User
roles []chronograf.Role
superAdmin bool
orgID string
}
tests := []struct {
name string
@ -646,6 +647,51 @@ func TestUsersStore_Update(t *testing.T) {
},
},
},
{
name: "Update user super admin",
fields: fields{
UsersStore: &mocks.UsersStore{
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{
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
SuperAdmin: false,
Roles: []chronograf.Role{
{
Organization: "1337",
Name: "viewer",
},
{
Organization: "1338",
Name: "editor",
},
},
}, nil
},
},
},
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{},
},
superAdmin: true,
orgID: "1338",
},
want: &chronograf.User{
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
SuperAdmin: true,
},
},
}
for _, tt := range tests {
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.orgID)
@ -655,6 +701,10 @@ func TestUsersStore_Update(t *testing.T) {
tt.args.usr.Roles = tt.args.roles
}
if tt.args.superAdmin {
tt.args.usr.SuperAdmin = tt.args.superAdmin
}
if err := s.Update(tt.args.ctx, tt.args.usr); (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
@ -664,6 +714,10 @@ func TestUsersStore_Update(t *testing.T) {
continue
}
if diff := cmp.Diff(tt.args.usr, tt.want, userCmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

View File

@ -150,18 +150,12 @@ const CheckSources = React.createClass({
const {
params,
sources,
auth: {isUsingAuth, me, me: {currentOrganization}},
auth: {isUsingAuth, me: {currentOrganization}},
} = this.props
const {isFetching} = this.state
const source = sources.find(s => s.id === params.sourceID)
if (
isFetching ||
!source ||
typeof isUsingAuth !== 'boolean' ||
(me && me.role === undefined) || // TODO: not sure this happens
!currentOrganization
) {
if (isFetching || !source || (isUsingAuth && !currentOrganization)) {
return <div className="page-spinner" />
}

View File

@ -26,9 +26,9 @@ class OrganizationsTable extends Component {
this.setState({isCreatingOrganization: false})
}
handleCreateOrganization = newOrganization => {
handleCreateOrganization = organization => {
const {onCreateOrg} = this.props
onCreateOrg(newOrganization)
onCreateOrg(organization)
this.setState({isCreatingOrganization: false})
}
@ -37,6 +37,7 @@ class OrganizationsTable extends Component {
organizations,
onDeleteOrg,
onRenameOrg,
onChooseDefaultRole,
onToggleWhitelistOnly,
} = this.props
const {isCreatingOrganization} = this.state
@ -87,12 +88,14 @@ class OrganizationsTable extends Component {
key={uuid.v4()}
organization={org}
onToggleWhitelistOnly={onToggleWhitelistOnly}
onChooseDefaultRole={onChooseDefaultRole}
/>
: <OrganizationsTableRow
key={uuid.v4()}
organization={org}
onDelete={onDeleteOrg}
onRename={onRenameOrg}
onChooseDefaultRole={onChooseDefaultRole}
/>
)}
</div>
@ -114,5 +117,6 @@ OrganizationsTable.propTypes = {
onDeleteOrg: func.isRequired,
onRenameOrg: func.isRequired,
onToggleWhitelistOnly: func.isRequired,
onChooseDefaultRole: func.isRequired,
}
export default OrganizationsTable

View File

@ -4,7 +4,6 @@ import ConfirmButtons from 'shared/components/ConfirmButtons'
import Dropdown from 'shared/components/Dropdown'
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
import {MEMBER_ROLE} from 'src/auth/Authorized'
class OrganizationsTableRow extends Component {
constructor(props) {
@ -14,7 +13,6 @@ class OrganizationsTableRow extends Component {
isEditing: false,
isDeleting: false,
workingName: this.props.organization.name,
defaultRole: MEMBER_ROLE,
}
}
@ -80,11 +78,12 @@ class OrganizationsTableRow extends Component {
}
handleChooseDefaultRole = role => {
this.setState({defaultRole: role.name})
const {organization, onChooseDefaultRole} = this.props
onChooseDefaultRole(organization, role.name)
}
render() {
const {workingName, isEditing, isDeleting, defaultRole} = this.state
const {workingName, isEditing, isDeleting} = this.state
const {organization} = this.props
const dropdownRolesItems = USER_ROLES.map(role => ({
@ -123,7 +122,7 @@ class OrganizationsTableRow extends Component {
<Dropdown
items={dropdownRolesItems}
onChoose={this.handleChooseDefaultRole}
selected={defaultRole}
selected={organization.defaultRole}
className="dropdown-stretch"
/>
</div>
@ -152,9 +151,11 @@ OrganizationsTableRow.propTypes = {
organization: shape({
id: string, // when optimistically created, organization will not have an id
name: string.isRequired,
defaultRole: string.isRequired,
}).isRequired,
onDelete: func.isRequired,
onRename: func.isRequired,
onChooseDefaultRole: func.isRequired,
}
export default OrganizationsTableRow

View File

@ -1,8 +1,9 @@
import React, {PropTypes, Component} from 'react'
import SlideToggle from 'shared/components/SlideToggle'
import Dropdown from 'shared/components/Dropdown'
import {MEMBER_ROLE} from 'src/auth/Authorized'
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
// This is a non-editable organization row, used currently for DEFAULT_ORG
class OrganizationsTableRowDefault extends Component {
@ -11,9 +12,19 @@ class OrganizationsTableRowDefault extends Component {
onToggleWhitelistOnly(organization)
}
handleChooseDefaultRole = role => {
const {organization, onChooseDefaultRole} = this.props
onChooseDefaultRole(organization, role.name)
}
render() {
const {organization} = this.props
const dropdownRolesItems = USER_ROLES.map(role => ({
...role,
text: role.name,
}))
return (
<div className="orgs-table--org">
<div className="orgs-table--id">
@ -29,8 +40,13 @@ class OrganizationsTableRowDefault extends Component {
onToggle={this.toggleWhitelistOnly}
/>
</div>
<div className="orgs-table--default-role-disabled">
{MEMBER_ROLE}
<div className="orgs-table--default-role">
<Dropdown
items={dropdownRolesItems}
onChoose={this.handleChooseDefaultRole}
selected={organization.defaultRole}
className="dropdown-stretch"
/>
</div>
<button
className="btn btn-sm btn-default btn-square orgs-table--delete"
@ -51,6 +67,7 @@ OrganizationsTableRowDefault.propTypes = {
name: string.isRequired,
}).isRequired,
onToggleWhitelistOnly: func.isRequired,
onChooseDefaultRole: func.isRequired,
}
export default OrganizationsTableRowDefault

View File

@ -39,7 +39,7 @@ class OrganizationsTableRowNew extends Component {
const {onCreateOrganization} = this.props
const {name, defaultRole} = this.state
onCreateOrganization(name, defaultRole)
onCreateOrganization({name, defaultRole})
}
handleChooseDefaultRole = role => {
@ -83,6 +83,7 @@ class OrganizationsTableRowNew extends Component {
disabled={isSaveDisabled}
onCancel={onCancelCreateOrganization}
onConfirm={this.handleClickSave}
confirmLeft={true}
/>
</div>
)

View File

@ -1,23 +0,0 @@
import React, {PropTypes} from 'react'
const PageHeader = ({currentOrganization}) =>
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">
{currentOrganization.name}
</h1>
</div>
</div>
</div>
const {shape, string} = PropTypes
PageHeader.propTypes = {
currentOrganization: shape({
id: string.isRequired,
name: string.isRequired,
}).isRequired,
}
export default PageHeader

View File

@ -23,8 +23,8 @@ class UsersTable extends Component {
this.props.onUpdateUserRole(user, currentRole, newRole)
}
handleChangeSuperAdmin = (user, currentStatus) => newStatus => {
this.props.onUpdateUserSuperAdmin(user, currentStatus, newStatus)
handleChangeSuperAdmin = user => newStatus => {
this.props.onUpdateUserSuperAdmin(user, newStatus)
}
handleDeleteUser = user => {
@ -57,6 +57,7 @@ class UsersTable extends Component {
numUsers={users.length}
onClickCreateUser={this.handleClickCreateUser}
isCreatingUser={isCreatingUser}
organization={organization}
/>
<div className="panel-body">
<table className="table table-highlight v-center chronograf-admin-table">
@ -74,7 +75,6 @@ class UsersTable extends Component {
<th style={{width: colProvider}}>Provider</th>
<th style={{width: colScheme}}>Scheme</th>
<th className="text-right" style={{width: colActions}} />
<th /* for DeleteConfirmTableCell */ />
</tr>
</thead>
<tbody>

View File

@ -7,14 +7,19 @@ class UsersTableHeader extends Component {
}
render() {
const {onClickCreateUser, numUsers, isCreatingUser} = this.props
const {
onClickCreateUser,
numUsers,
isCreatingUser,
organization,
} = this.props
const panelTitle = numUsers === 1 ? `${numUsers} User` : `${numUsers} Users`
return (
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{panelTitle}
{panelTitle} in <em>{organization.name}</em>
</h2>
<Authorized requiredRole={ADMIN_ROLE}>
<button
@ -31,12 +36,16 @@ class UsersTableHeader extends Component {
}
}
const {bool, func, number} = PropTypes
const {bool, func, shape, string, number} = PropTypes
UsersTableHeader.propTypes = {
numUsers: number.isRequired,
onClickCreateUser: func.isRequired,
isCreatingUser: bool.isRequired,
organization: shape({
name: string.isRequired,
id: string.isRequired,
}),
}
export default UsersTableHeader

View File

@ -16,13 +16,7 @@ const UsersTableRow = ({
onChangeSuperAdmin,
onDelete,
}) => {
const {
colRole,
colSuperAdmin,
colProvider,
colScheme,
colActions,
} = USERS_TABLE
const {colRole, colSuperAdmin, colProvider, colScheme} = USERS_TABLE
const dropdownRolesItems = USER_ROLES.map(r => ({
...r,
@ -55,7 +49,7 @@ const UsersTableRow = ({
<td style={{width: colSuperAdmin}} className="text-center">
<SlideToggle
active={user.superAdmin}
onToggle={onChangeSuperAdmin(user, user.superAdmin)}
onToggle={onChangeSuperAdmin(user)}
size="xs"
/>
</td>
@ -66,7 +60,6 @@ const UsersTableRow = ({
<td style={{width: colScheme}}>
{user.scheme}
</td>
<td className="text-right" style={{width: colActions}} />
<DeleteConfirmTableCell
text="Remove"
onDelete={onDelete}

View File

@ -34,7 +34,7 @@ class UsersTableRowNew extends Component {
name,
provider,
scheme,
superAdmin: superAdmin.value,
superAdmin,
roles: [
{
name: role,

View File

@ -3,5 +3,5 @@ export const USERS_TABLE = {
colSuperAdmin: 90,
colProvider: 170,
colScheme: 90,
colActions: 68,
colActions: 80,
}

View File

@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux'
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import PageHeader from 'src/admin/components/chronograf/PageHeader'
import AdminTabs from 'src/admin/components/chronograf/AdminTabs'
import FancyScrollbar from 'shared/components/FancyScrollbar'
@ -39,7 +38,7 @@ class AdminChronografPage extends Component {
createUserAsync(links.users, user)
}
handleUpdateUserRole = () => (user, currentRole, {name}) => {
handleUpdateUserRole = (user, currentRole, {name}) => {
const {actions: {updateUserAsync}} = this.props
const updatedRole = {...currentRole, name}
@ -49,14 +48,16 @@ class AdminChronografPage extends Component {
updateUserAsync(user, {...user, roles: newRoles})
}
handleUpdateUserSuperAdmin = () => (user, currentStatus, {value}) => {
handleUpdateUserSuperAdmin = (user, superAdmin) => {
const {actions: {updateUserAsync}} = this.props
const updatedUser = {...user, superAdmin: value}
const updatedUser = {...user, superAdmin}
updateUserAsync(user, updatedUser)
}
handleDeleteUser = () => user => {
handleDeleteUser = user => {
const {actions: {deleteUserAsync}} = this.props
deleteUserAsync(user)
@ -67,23 +68,27 @@ class AdminChronografPage extends Component {
return (
<div className="page">
<PageHeader currentOrganization={currentOrganization} />
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Chronograf Admin</h1>
</div>
</div>
</div>
<FancyScrollbar className="page-contents">
{users
? <div className="container-fluid">
<div className="row">
<div className="col-xs-12">
<AdminTabs
meRole={meRole}
// UsersTable
users={users}
organization={currentOrganization}
onCreateUser={this.handleCreateUser}
onUpdateUserRole={this.handleUpdateUserRole()}
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin()}
onDeleteUser={this.handleDeleteUser()}
/>
</div>
<AdminTabs
meRole={meRole}
// UsersTable
users={users}
organization={currentOrganization}
onCreateUser={this.handleCreateUser}
onUpdateUserRole={this.handleUpdateUserRole}
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
onDeleteUser={this.handleDeleteUser}
/>
</div>
</div>
: <div className="page-spinner" />}

View File

@ -14,9 +14,9 @@ class OrganizationsPage extends Component {
loadOrganizationsAsync(links.organizations)
}
handleCreateOrganization = organizationName => {
handleCreateOrganization = organization => {
const {links, actions: {createOrganizationAsync}} = this.props
createOrganizationAsync(links.organizations, {name: organizationName})
createOrganizationAsync(links.organizations, organization)
}
handleRenameOrganization = (organization, name) => {
@ -35,6 +35,11 @@ class OrganizationsPage extends Component {
updateOrganizationAsync(organization, {...organization, whitelistOnly})
}
handleChooseDefaultRole = (organization, defaultRole) => {
const {actions: {updateOrganizationAsync}} = this.props
updateOrganizationAsync(organization, {...organization, defaultRole})
}
render() {
const {organizations} = this.props
@ -45,6 +50,7 @@ class OrganizationsPage extends Component {
onDeleteOrg={this.handleDeleteOrganization}
onRenameOrg={this.handleRenameOrganization}
onToggleWhitelistOnly={this.handleToggleWhitelistOnly}
onChooseDefaultRole={this.handleChooseDefaultRole}
/>
)
}

View File

@ -47,9 +47,7 @@ input[type="text"].form-control.orgs-table--input {
background-color: $g2-kevlar;
color: $g13-mist;
position: relative;
transition:
color 0.4s ease,
background-color 0.4s ease,
transition: color 0.4s ease, background-color 0.4s ease,
border-color 0.4s ease;
> span.icon {
@ -68,7 +66,9 @@ input[type="text"].form-control.orgs-table--input {
border-color: $g5-pepper;
cursor: text;
> span.icon {opacity: 1;}
> span.icon {
opacity: 1;
}
}
}
.orgs-table--name-disabled {
@ -92,7 +92,7 @@ input[type="text"].form-control.orgs-table--input {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
transform: translate(-50%, -50%);
}
&.disabled {
@ -125,7 +125,6 @@ input[type="text"].form-control.orgs-table--input {
width: 30px;
}
/* Table Headers */
.orgs-table--org-labels {
display: flex;
@ -148,8 +147,8 @@ input[type="text"].form-control.orgs-table--input {
> .orgs-table--name:hover,
> .orgs-table--default-role,
> .orgs-table--whitelist {
color: $g15-platinum;
font-weight: 700;
color: $g17-whisper;
font-weight: 500;
}
> .orgs-table--default-role,
> .orgs-table--whitelist {