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