mirror of
https://github.com/ansible/awx.git
synced 2026-02-18 03:30:02 -03:30
Add RBAC to org views
This commit is contained in:
@@ -39,7 +39,7 @@ class AWXLogin extends Component {
|
||||
|
||||
async onLoginButtonClick (event) {
|
||||
const { username, password, isLoading } = this.state;
|
||||
const { api, handleHttpError, clearRootDialogMessage } = this.props;
|
||||
const { api, handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -51,7 +51,9 @@ class AWXLogin extends Component {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
try {
|
||||
await api.login(username, password);
|
||||
const { data } = await api.login(username, password);
|
||||
updateConfig(data);
|
||||
fetchMe();
|
||||
this.setState({ isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Route, withRouter, Switch } from 'react-router-dom';
|
||||
import { i18nMark } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { Config } from '../../contexts/Config';
|
||||
import { NetworkProvider } from '../../contexts/Network';
|
||||
import { withRootDialog } from '../../contexts/RootDialog';
|
||||
|
||||
@@ -74,11 +75,16 @@ class Organizations extends Component {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Organization
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadcrumbConfig}
|
||||
/>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Organization
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadcrumbConfig}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
</NetworkProvider>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,9 +18,14 @@ import {
|
||||
withRouter
|
||||
} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
PlusIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import { withNetwork } from '../../../contexts/Network';
|
||||
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import BasicChip from '../../../components/BasicChip/BasicChip';
|
||||
import Pagination from '../../../components/Pagination';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
||||
@@ -357,6 +362,7 @@ class OrganizationAccessList extends React.Component {
|
||||
columns={this.columns}
|
||||
onSearch={() => { }}
|
||||
onSort={this.onSort}
|
||||
showAdd={organization.summary_fields.user_capabilities.edit}
|
||||
add={(
|
||||
<Fragment>
|
||||
<Button
|
||||
@@ -421,13 +427,21 @@ class OrganizationAccessList extends React.Component {
|
||||
<ul style={userRolesWrapperStyle}>
|
||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
|
||||
{result.userRoles.map(role => (
|
||||
<Chip
|
||||
key={role.id}
|
||||
className="awx-c-chip"
|
||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||
>
|
||||
{role.name}
|
||||
</Chip>
|
||||
role.user_capabilities.unattach ? (
|
||||
<Chip
|
||||
key={role.id}
|
||||
className="awx-c-chip"
|
||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||
>
|
||||
{role.name}
|
||||
</Chip>
|
||||
) : (
|
||||
<BasicChip
|
||||
key={role.id}
|
||||
>
|
||||
{role.name}
|
||||
</BasicChip>
|
||||
)
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -33,13 +33,17 @@ class Organization extends Component {
|
||||
organization: null,
|
||||
error: false,
|
||||
loading: true,
|
||||
isNotifAdmin: false,
|
||||
isAuditorOfThisOrg: false,
|
||||
isAdminOfThisOrg: false
|
||||
};
|
||||
|
||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchOrganization();
|
||||
this.fetchOrganizationAndRoles();
|
||||
}
|
||||
|
||||
async componentDidUpdate (prevProps) {
|
||||
@@ -49,6 +53,43 @@ class Organization extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOrganizationAndRoles () {
|
||||
const {
|
||||
match,
|
||||
setBreadcrumb,
|
||||
api,
|
||||
handleHttpError
|
||||
} = this.props;
|
||||
|
||||
try {
|
||||
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
|
||||
api.getOrganizationDetails(parseInt(match.params.id, 10)),
|
||||
api.getOrganizations({
|
||||
role_level: 'notification_admin_role',
|
||||
page_size: 1
|
||||
}),
|
||||
api.getOrganizations({
|
||||
role_level: 'auditor_role',
|
||||
id: parseInt(match.params.id, 10)
|
||||
}),
|
||||
api.getOrganizations({
|
||||
role_level: 'admin_role',
|
||||
id: parseInt(match.params.id, 10)
|
||||
})
|
||||
]);
|
||||
setBreadcrumb(data);
|
||||
this.setState({
|
||||
organization: data,
|
||||
loading: false,
|
||||
isNotifAdmin: notifAdminRest.data.results.length > 0,
|
||||
isAuditorOfThisOrg: auditorRes.data.results.length > 0,
|
||||
isAdminOfThisOrg: adminRes.data.results.length > 0
|
||||
});
|
||||
} catch (error) {
|
||||
handleHttpError(error) || this.setState({ error: true, loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOrganization () {
|
||||
const {
|
||||
match,
|
||||
@@ -70,19 +111,40 @@ class Organization extends Component {
|
||||
const {
|
||||
location,
|
||||
match,
|
||||
me,
|
||||
history
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
organization,
|
||||
error,
|
||||
loading
|
||||
loading,
|
||||
isNotifAdmin,
|
||||
isAuditorOfThisOrg,
|
||||
isAdminOfThisOrg
|
||||
} = this.state;
|
||||
|
||||
const tabsPaddingOverride = {
|
||||
padding: '0'
|
||||
};
|
||||
|
||||
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg;
|
||||
const canToggleNotifications = isNotifAdmin && (
|
||||
me.is_system_auditor
|
||||
|| isAuditorOfThisOrg
|
||||
|| isAdminOfThisOrg
|
||||
);
|
||||
|
||||
const tabElements = [
|
||||
{ name: i18nMark('Details'), link: `${match.url}/details` },
|
||||
{ name: i18nMark('Access'), link: `${match.url}/access` },
|
||||
{ name: i18nMark('Teams'), link: `${match.url}/teams` }
|
||||
];
|
||||
|
||||
if (canSeeNotificationsTab) {
|
||||
tabElements.push({ name: i18nMark('Notifications'), link: `${match.url}/notifications` });
|
||||
}
|
||||
|
||||
let cardHeader = (
|
||||
loading ? ''
|
||||
: (
|
||||
@@ -174,16 +236,19 @@ class Organization extends Component {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/organizations/:id/notifications"
|
||||
render={() => (
|
||||
<OrganizationNotifications
|
||||
match={match}
|
||||
location={location}
|
||||
history={history}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{canSeeNotificationsTab && (
|
||||
<Route
|
||||
path="/organizations/:id/notifications"
|
||||
render={() => (
|
||||
<OrganizationNotifications
|
||||
match={match}
|
||||
location={location}
|
||||
history={history}
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
||||
</Switch>
|
||||
{error ? 'error!' : ''}
|
||||
|
||||
@@ -100,7 +100,8 @@ class OrganizationDetail extends Component {
|
||||
description,
|
||||
custom_virtualenv,
|
||||
created,
|
||||
modified
|
||||
modified,
|
||||
summary_fields
|
||||
},
|
||||
match
|
||||
} = this.props;
|
||||
@@ -165,11 +166,13 @@ class OrganizationDetail extends Component {
|
||||
</TextContent>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||
<Button><Trans>Edit</Trans></Button>
|
||||
</Link>
|
||||
</div>
|
||||
{summary_fields.user_capabilities.edit && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||
<Button><Trans>Edit</Trans></Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{error ? 'error!' : ''}
|
||||
</CardBody>
|
||||
)}
|
||||
|
||||
@@ -41,13 +41,18 @@ class OrganizationNotifications extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
canToggleNotifications
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<NotificationsList
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
onCreateError={this.createOrgNotificationError}
|
||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||
onReadError={this.readOrgNotificationError}
|
||||
onReadNotifications={this.readOrgNotifications}
|
||||
onReadSuccess={this.readOrgNotificationSuccess}
|
||||
onReadError={this.readOrgNotificationError}
|
||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||
onCreateError={this.createOrgNotificationError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ class OrganizationsList extends Component {
|
||||
loading: true,
|
||||
results: [],
|
||||
selected: [],
|
||||
isModalOpen: false,
|
||||
orgsToDelete: [],
|
||||
isModalOpen: false
|
||||
|
||||
};
|
||||
|
||||
@@ -74,14 +73,16 @@ class OrganizationsList extends Component {
|
||||
this.onSelectAll = this.onSelectAll.bind(this);
|
||||
this.onSelect = this.onSelect.bind(this);
|
||||
this.updateUrl = this.updateUrl.bind(this);
|
||||
this.callOrganizations = this.callOrganizations.bind(this);
|
||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
||||
this.handleClearOrgsToDelete = this.handleClearOrgsToDelete.bind(this);
|
||||
this.handleCloseOrgDeleteModal = this.handleCloseOrgDeleteModal.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const queryParams = this.getQueryParams();
|
||||
this.callOrganizations();
|
||||
this.fetchOrganizations(queryParams);
|
||||
}
|
||||
|
||||
@@ -117,20 +118,20 @@ class OrganizationsList extends Component {
|
||||
onSelectAll (isSelected) {
|
||||
const { results } = this.state;
|
||||
|
||||
const selected = isSelected ? results.map(o => o.id) : [];
|
||||
const selected = isSelected ? results : [];
|
||||
|
||||
this.setState({ selected });
|
||||
}
|
||||
|
||||
onSelect (id) {
|
||||
onSelect (row) {
|
||||
const { selected } = this.state;
|
||||
|
||||
const isSelected = selected.includes(id);
|
||||
const isSelected = selected.some(s => s.id === row.id);
|
||||
|
||||
if (isSelected) {
|
||||
this.setState({ selected: selected.filter(s => s !== id) });
|
||||
this.setState({ selected: selected.filter(s => s.id !== row.id) });
|
||||
} else {
|
||||
this.setState({ selected: selected.concat(id) });
|
||||
this.setState({ selected: selected.concat(row) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,43 +144,35 @@ class OrganizationsList extends Component {
|
||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||
}
|
||||
|
||||
handleClearOrgsToDelete () {
|
||||
handleCloseOrgDeleteModal () {
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
orgsToDelete: []
|
||||
isModalOpen: false
|
||||
});
|
||||
this.onSelectAll();
|
||||
}
|
||||
|
||||
handleOpenOrgDeleteModal () {
|
||||
const { results, selected } = this.state;
|
||||
const { selected } = this.state;
|
||||
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
||||
const warningMsg = i18nMark('Are you sure you want to delete:');
|
||||
|
||||
const orgsToDelete = [];
|
||||
results.forEach((result) => {
|
||||
selected.forEach((selectedOrg) => {
|
||||
if (result.id === selectedOrg) {
|
||||
orgsToDelete.push({ name: result.name, id: selectedOrg });
|
||||
}
|
||||
});
|
||||
});
|
||||
this.setState({
|
||||
orgsToDelete,
|
||||
isModalOpen: true,
|
||||
warningTitle,
|
||||
warningMsg,
|
||||
loading: false });
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
async handleOrgDelete (event) {
|
||||
const { orgsToDelete } = this.state;
|
||||
async handleOrgDelete () {
|
||||
const { selected } = this.state;
|
||||
const { api, handleHttpError } = this.props;
|
||||
let errorHandled;
|
||||
|
||||
try {
|
||||
await Promise.all(orgsToDelete.map((org) => api.destroyOrganization(org.id)));
|
||||
this.handleClearOrgsToDelete();
|
||||
await Promise.all(selected.map((org) => api.destroyOrganization(org.id)));
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
selected: []
|
||||
});
|
||||
} catch (err) {
|
||||
errorHandled = handleHttpError(err);
|
||||
} finally {
|
||||
@@ -188,7 +181,6 @@ class OrganizationsList extends Component {
|
||||
this.fetchOrganizations(queryParams);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
updateUrl (queryParams) {
|
||||
@@ -248,16 +240,35 @@ class OrganizationsList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async callOrganizations () {
|
||||
const { api } = this.props;
|
||||
|
||||
try {
|
||||
const { data } = await api.callOrganizations();
|
||||
const { actions } = data;
|
||||
|
||||
const stateToUpdate = {
|
||||
canAdd: Object.prototype.hasOwnProperty.call(actions, 'POST')
|
||||
};
|
||||
|
||||
this.setState(stateToUpdate);
|
||||
} catch (err) {
|
||||
this.setState({ error: true });
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
medium,
|
||||
} = PageSectionVariants;
|
||||
const {
|
||||
canAdd,
|
||||
count,
|
||||
error,
|
||||
loading,
|
||||
noInitialResults,
|
||||
orgsToDelete,
|
||||
page,
|
||||
pageCount,
|
||||
page_size,
|
||||
@@ -270,6 +281,12 @@ class OrganizationsList extends Component {
|
||||
warningMsg,
|
||||
} = this.state;
|
||||
const { match } = this.props;
|
||||
|
||||
const disableDelete = (
|
||||
selected.length === 0
|
||||
|| selected.some(row => !row.summary_fields.user_capabilities.delete)
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -280,15 +297,15 @@ class OrganizationsList extends Component {
|
||||
variant="danger"
|
||||
title={warningTitle}
|
||||
isOpen={isModalOpen}
|
||||
onClose={this.handleClearOrgsToDelete}
|
||||
onClose={this.handleCloseOrgDeleteModal}
|
||||
actions={[
|
||||
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
||||
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>{i18n._(t`Cancel`)}</Button>
|
||||
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleCloseOrgDeleteModal}>{i18n._(t`Cancel`)}</Button>
|
||||
]}
|
||||
>
|
||||
{warningMsg}
|
||||
<br />
|
||||
{orgsToDelete.map((org) => (
|
||||
{selected.map((org) => (
|
||||
<span key={org.id}>
|
||||
<strong>
|
||||
{org.name}
|
||||
@@ -321,9 +338,24 @@ class OrganizationsList extends Component {
|
||||
onSort={this.onSort}
|
||||
onSelectAll={this.onSelectAll}
|
||||
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
||||
disableTrashCanIcon={selected.length === 0}
|
||||
disableTrashCanIcon={disableDelete}
|
||||
deleteTooltip={
|
||||
selected.some(row => !row.summary_fields.user_capabilities.delete) ? (
|
||||
<div>
|
||||
<Trans>
|
||||
You dont have permission to delete the following Organizations:
|
||||
</Trans>
|
||||
{selected.map(row => (
|
||||
<div key={row.id}>
|
||||
{row.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
showDelete
|
||||
showSelectAll
|
||||
showAdd={canAdd}
|
||||
/>
|
||||
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
||||
{ results.map(o => (
|
||||
@@ -334,8 +366,8 @@ class OrganizationsList extends Component {
|
||||
detailUrl={`${match.url}/${o.id}`}
|
||||
memberCount={o.summary_fields.related_field_counts.users}
|
||||
teamCount={o.summary_fields.related_field_counts.teams}
|
||||
isSelected={selected.includes(o.id)}
|
||||
onSelect={() => this.onSelect(o.id, o.name)}
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => this.onSelect(o)}
|
||||
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user