Merge pull request #12358 from nixocio/ui_issue_5883

Hide add access button based on the user profile for credentials
This commit is contained in:
Sarah Akus 2022-06-13 13:38:36 -04:00 committed by GitHub
commit 7dbf5f7138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 376 additions and 5 deletions

View File

@ -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;

View File

@ -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"

View File

@ -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);
});
});