mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 23:16:01 -03:30
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:
@@ -77,6 +77,10 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
|||||||
disassociate: true,
|
disassociate: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readAdmins(id, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/admins/`, { params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Organizations;
|
export default Organizations;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
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 { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
|
import { useUserProfile, useConfig } from 'contexts/Config';
|
||||||
import AddResourceRole from '../AddRole/AddResourceRole';
|
import AddResourceRole from '../AddRole/AddResourceRole';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
@@ -24,6 +25,8 @@ const QS_CONFIG = getQSConfig('access', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ResourceAccessList({ apiModel, resource }) {
|
function ResourceAccessList({ apiModel, resource }) {
|
||||||
|
const { isSuperUser, isOrgAdmin } = useUserProfile();
|
||||||
|
const { me } = useConfig();
|
||||||
const [submitError, setSubmitError] = useState(null);
|
const [submitError, setSubmitError] = useState(null);
|
||||||
const [deletionRecord, setDeletionRecord] = useState(null);
|
const [deletionRecord, setDeletionRecord] = useState(null);
|
||||||
const [deletionRole, setDeletionRole] = useState(null);
|
const [deletionRole, setDeletionRole] = useState(null);
|
||||||
@@ -31,6 +34,49 @@ function ResourceAccessList({ apiModel, resource }) {
|
|||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const location = useLocation();
|
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 {
|
const {
|
||||||
result: {
|
result: {
|
||||||
accessRecords,
|
accessRecords,
|
||||||
@@ -149,8 +195,8 @@ function ResourceAccessList({ apiModel, resource }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
error={contentError}
|
error={contentError || errorFetchingOrgAdmins}
|
||||||
hasContentLoading={isLoading || isDeleteLoading}
|
hasContentLoading={isLoading || isDeleteLoading || isFetchingOrgAdmins}
|
||||||
items={accessRecords}
|
items={accessRecords}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
pluralizedItemName={t`Roles`}
|
pluralizedItemName={t`Roles`}
|
||||||
@@ -163,7 +209,7 @@ function ResourceAccessList({ apiModel, resource }) {
|
|||||||
{...props}
|
{...props}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={
|
additionalControls={
|
||||||
resource?.summary_fields?.user_capabilities?.edit
|
canAddAdditionalControls
|
||||||
? [
|
? [
|
||||||
<ToolbarAddButton
|
<ToolbarAddButton
|
||||||
ouiaId="access-add-button"
|
ouiaId="access-add-button"
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
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 {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
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({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/organizations/1/access'],
|
initialEntries: ['/organizations/1/access'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const credentialHistory = createMemoryHistory({
|
||||||
|
initialEntries: ['/credentials/1/access'],
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
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.readAccessList.mockResolvedValue({ data });
|
||||||
OrganizationsAPI.readAccessOptions.mockResolvedValue({
|
OrganizationsAPI.readAccessOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -106,6 +330,7 @@ describe('<ResourceAccessList />', () => {
|
|||||||
related_search_fields: [],
|
related_search_fields: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
OrganizationsAPI.readAdmins.mockResolvedValue({ data: { count: 1 } });
|
||||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||||
UsersAPI.disassociateRole.mockResolvedValue({});
|
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||||
RolesAPI.read.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 () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user