mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Hide add access based on the user profile for credentials
* Show add access button if it is a system admin * Hide access button if the user is credential admin, org admin, but the credential does not belong to any org. * Show access button if the user is a credential admin, org admin, and the credential is associated to an org. * Show access button if the user is an org admin and the credential is associated to the org. All those permutations are allowed by the API RBAC. This PR update UX to not allow the user to attempt to perform any action that will raise an error when modifying access to the credentials.
This commit is contained in:
parent
23aaf5b3ad
commit
96330f608d
@ -77,6 +77,10 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readAdmins(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/admins/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { RolesAPI, TeamsAPI, UsersAPI } from 'api';
|
||||
import { RolesAPI, TeamsAPI, UsersAPI, OrganizationsAPI } from 'api';
|
||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||
import { useUserProfile, useConfig } from 'contexts/Config';
|
||||
import AddResourceRole from '../AddRole/AddResourceRole';
|
||||
import AlertModal from '../AlertModal';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
@ -24,6 +25,8 @@ const QS_CONFIG = getQSConfig('access', {
|
||||
});
|
||||
|
||||
function ResourceAccessList({ apiModel, resource }) {
|
||||
const { isSuperUser, isOrgAdmin } = useUserProfile();
|
||||
const { me } = useConfig();
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
const [deletionRecord, setDeletionRecord] = useState(null);
|
||||
const [deletionRole, setDeletionRole] = useState(null);
|
||||
@ -31,6 +34,49 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
isLoading: isFetchingOrgAdmins,
|
||||
error: errorFetchingOrgAdmins,
|
||||
request: fetchOrgAdmins,
|
||||
result: { isCredentialOrgAdmin },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
if (
|
||||
isSuperUser ||
|
||||
resource.type !== 'credential' ||
|
||||
!isOrgAdmin ||
|
||||
!resource?.organization
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
data: { count },
|
||||
} = await OrganizationsAPI.readAdmins(resource.organization, {
|
||||
id: me.id,
|
||||
});
|
||||
return { isCredentialOrgAdmin: !!count };
|
||||
}, [me.id, isOrgAdmin, isSuperUser, resource.type, resource.organization]),
|
||||
{
|
||||
isCredentialOrgAdmin: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrgAdmins();
|
||||
}, [fetchOrgAdmins]);
|
||||
|
||||
let canAddAdditionalControls = false;
|
||||
if (isSuperUser) {
|
||||
canAddAdditionalControls = true;
|
||||
}
|
||||
if (resource.type === 'credential' && isOrgAdmin && isCredentialOrgAdmin) {
|
||||
canAddAdditionalControls = true;
|
||||
}
|
||||
if (resource.type !== 'credential') {
|
||||
canAddAdditionalControls =
|
||||
resource?.summary_fields?.user_capabilities?.edit;
|
||||
}
|
||||
|
||||
const {
|
||||
result: {
|
||||
accessRecords,
|
||||
@ -149,8 +195,8 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
error={contentError}
|
||||
hasContentLoading={isLoading || isDeleteLoading}
|
||||
error={contentError || errorFetchingOrgAdmins}
|
||||
hasContentLoading={isLoading || isDeleteLoading || isFetchingOrgAdmins}
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={t`Roles`}
|
||||
@ -163,7 +209,7 @@ function ResourceAccessList({ apiModel, resource }) {
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={
|
||||
resource?.summary_fields?.user_capabilities?.edit
|
||||
canAddAdditionalControls
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
ouiaId="access-add-button"
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from 'api';
|
||||
import {
|
||||
CredentialsAPI,
|
||||
OrganizationsAPI,
|
||||
RolesAPI,
|
||||
TeamsAPI,
|
||||
UsersAPI,
|
||||
} from 'api';
|
||||
import { useUserProfile } from 'contexts/Config';
|
||||
import * as ConfigContext from 'contexts/Config';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -91,11 +99,227 @@ describe('<ResourceAccessList />', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const credentialAccessList = {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
url: '/api/v2/users/1/',
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 20,
|
||||
name: 'Admin',
|
||||
description: 'Can manage all aspects of the credential',
|
||||
resource_name: 'Demo Credential',
|
||||
resource_type: 'credential',
|
||||
related: { credential: '/api/v2/credentials/1/' },
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['admin_role', 'read_role', 'use_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [
|
||||
{
|
||||
role: {
|
||||
id: 1,
|
||||
name: 'System Administrator',
|
||||
description: 'Can manage all aspects of the system',
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['admin_role', 'read_role', 'use_role'],
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2022-06-08T18:31:35.834036Z',
|
||||
modified: '2022-06-09T16:47:54.712473Z',
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: 'admin@localhost',
|
||||
is_superuser: true,
|
||||
is_system_auditor: false,
|
||||
ldap_dn: '',
|
||||
last_login: '2022-06-09T16:47:54.712473Z',
|
||||
external_account: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'user',
|
||||
url: '/api/v2/users/2/',
|
||||
related: {
|
||||
teams: '/api/v2/users/2/teams/',
|
||||
organizations: '/api/v2/users/2/organizations/',
|
||||
admin_of_organizations: '/api/v2/users/2/admin_of_organizations/',
|
||||
projects: '/api/v2/users/2/projects/',
|
||||
credentials: '/api/v2/users/2/credentials/',
|
||||
roles: '/api/v2/users/2/roles/',
|
||||
activity_stream: '/api/v2/users/2/activity_stream/',
|
||||
access_list: '/api/v2/users/2/access_list/',
|
||||
tokens: '/api/v2/users/2/tokens/',
|
||||
authorized_tokens: '/api/v2/users/2/authorized_tokens/',
|
||||
personal_tokens: '/api/v2/users/2/personal_tokens/',
|
||||
},
|
||||
summary_fields: {
|
||||
direct_access: [
|
||||
{
|
||||
role: {
|
||||
id: 22,
|
||||
name: 'Read',
|
||||
description: 'May view settings for the credential',
|
||||
resource_name: 'Demo Credential',
|
||||
resource_type: 'credential',
|
||||
related: { credential: '/api/v2/credentials/1/' },
|
||||
user_capabilities: { unattach: false },
|
||||
},
|
||||
descendant_roles: ['read_role'],
|
||||
},
|
||||
],
|
||||
indirect_access: [],
|
||||
},
|
||||
created: '2022-06-09T13:45:56.049783Z',
|
||||
modified: '2022-06-09T16:48:46.169760Z',
|
||||
username: 'second',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
is_superuser: false,
|
||||
is_system_auditor: false,
|
||||
ldap_dn: '',
|
||||
last_login: '2022-06-09T16:48:46.169760Z',
|
||||
external_account: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const credential = {
|
||||
id: 1,
|
||||
type: 'credential',
|
||||
url: '/api/v2/credentials/1/',
|
||||
related: {
|
||||
named_url: '/api/v2/credentials/Demo Credential++Machine+ssh++Default/',
|
||||
created_by: '/api/v2/users/1/',
|
||||
modified_by: '/api/v2/users/1/',
|
||||
organization: '/api/v2/organizations/1/',
|
||||
activity_stream: '/api/v2/credentials/1/activity_stream/',
|
||||
access_list: '/api/v2/credentials/1/access_list/',
|
||||
object_roles: '/api/v2/credentials/1/object_roles/',
|
||||
owner_users: '/api/v2/credentials/1/owner_users/',
|
||||
owner_teams: '/api/v2/credentials/1/owner_teams/',
|
||||
copy: '/api/v2/credentials/1/copy/',
|
||||
input_sources: '/api/v2/credentials/1/input_sources/',
|
||||
credential_type: '/api/v2/credential_types/1/',
|
||||
},
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
description: '',
|
||||
},
|
||||
credential_type: {
|
||||
id: 1,
|
||||
name: 'Machine',
|
||||
description: '',
|
||||
},
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
modified_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
object_roles: {
|
||||
admin_role: {
|
||||
description: 'Can manage all aspects of the credential',
|
||||
name: 'Admin',
|
||||
id: 20,
|
||||
},
|
||||
use_role: {
|
||||
description: 'Can use the credential in a job template',
|
||||
name: 'Use',
|
||||
id: 21,
|
||||
},
|
||||
read_role: {
|
||||
description: 'May view settings for the credential',
|
||||
name: 'Read',
|
||||
id: 22,
|
||||
},
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
copy: false,
|
||||
use: true,
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
id: 3,
|
||||
type: 'user',
|
||||
name: 'third',
|
||||
description: ' ',
|
||||
url: '/api/v2/users/3/',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
name: 'admin',
|
||||
description: ' ',
|
||||
url: '/api/v2/users/1/',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
type: 'organization',
|
||||
name: 'Default',
|
||||
description: '',
|
||||
url: '/api/v2/organizations/1/',
|
||||
},
|
||||
],
|
||||
},
|
||||
created: '2022-06-08T18:31:43.491973Z',
|
||||
modified: '2022-06-09T19:40:49.460771Z',
|
||||
name: 'Demo Credential',
|
||||
description: '',
|
||||
organization: 1,
|
||||
credential_type: 1,
|
||||
managed: false,
|
||||
inputs: {
|
||||
username: 'admin',
|
||||
become_method: '',
|
||||
become_username: '',
|
||||
},
|
||||
kind: 'ssh',
|
||||
cloud: false,
|
||||
kubernetes: false,
|
||||
};
|
||||
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/organizations/1/access'],
|
||||
});
|
||||
|
||||
const credentialHistory = createMemoryHistory({
|
||||
initialEntries: ['/credentials/1/access'],
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(ConfigContext, 'useConfig').mockImplementation(() => ({
|
||||
me: { id: 2 },
|
||||
}));
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: true,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: false,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
|
||||
OrganizationsAPI.readAccessOptions.mockResolvedValue({
|
||||
data: {
|
||||
@ -106,6 +330,7 @@ describe('<ResourceAccessList />', () => {
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
OrganizationsAPI.readAdmins.mockResolvedValue({ data: { count: 1 } });
|
||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||
RolesAPI.read.mockResolvedValue({
|
||||
@ -116,6 +341,16 @@ describe('<ResourceAccessList />', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
CredentialsAPI.readAccessList.mockResolvedValue({ credentialAccessList });
|
||||
CredentialsAPI.readAccessOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
related_search_fields: [],
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@ -213,4 +448,90 @@ describe('<ResourceAccessList />', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should show add button for system admin', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(1);
|
||||
});
|
||||
|
||||
test('should not show add button for non system admin & non org admin', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: false,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should show add button for non system admin, org admin, credential admin for credentials associated with org', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: true,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={credential} apiModel={CredentialsAPI} />,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(1);
|
||||
});
|
||||
|
||||
test('should not show add button for non system admin, org admin, credential admin for credentials non associated with org', async () => {
|
||||
useUserProfile.mockImplementation(() => {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
isSystemAuditor: false,
|
||||
isOrgAdmin: true,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
};
|
||||
});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList
|
||||
resource={{ ...credential, organization: null }}
|
||||
apiModel={CredentialsAPI}
|
||||
/>,
|
||||
{ context: { router: { credentialHistory } } }
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ToolbarAddButton').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user