diff --git a/awx/ui/src/api/models/Organizations.js b/awx/ui/src/api/models/Organizations.js index ac4266d411..c20f72a181 100644 --- a/awx/ui/src/api/models/Organizations.js +++ b/awx/ui/src/api/models/Organizations.js @@ -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; diff --git a/awx/ui/src/components/ResourceAccessList/ResourceAccessList.js b/awx/ui/src/components/ResourceAccessList/ResourceAccessList.js index 0817928fbd..c4c37c057e 100644 --- a/awx/ui/src/components/ResourceAccessList/ResourceAccessList.js +++ b/awx/ui/src/components/ResourceAccessList/ResourceAccessList.js @@ -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 ( <> ', () => { ], }; + 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('', () => { related_search_fields: [], }, }); + OrganizationsAPI.readAdmins.mockResolvedValue({ data: { count: 1 } }); TeamsAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({}); RolesAPI.read.mockResolvedValue({ @@ -116,6 +341,16 @@ describe('', () => { ], }, }); + CredentialsAPI.readAccessList.mockResolvedValue({ credentialAccessList }); + CredentialsAPI.readAccessOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( @@ -213,4 +448,90 @@ describe('', () => { }, ]); }); + + test('should show add button for system admin', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + , + { 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( + , + { 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( + , + { 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( + , + { context: { router: { credentialHistory } } } + ); + }); + + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toEqual(0); + }); });