From 96330f608db08fe2f90ffb6d0a999457f6728a69 Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 9 Jun 2022 10:11:12 -0400 Subject: [PATCH] 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. --- awx/ui/src/api/models/Organizations.js | 4 + .../ResourceAccessList/ResourceAccessList.js | 54 ++- .../ResourceAccessList.test.js | 323 +++++++++++++++++- 3 files changed, 376 insertions(+), 5 deletions(-) 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); + }); });