diff --git a/app/controllers/ui_job_wizard_controller.rb b/app/controllers/ui_job_wizard_controller.rb index b9ae20f8d..20a75472f 100644 --- a/app/controllers/ui_job_wizard_controller.rb +++ b/app/controllers/ui_job_wizard_controller.rb @@ -1,4 +1,4 @@ -class UiJobWizardController < ApplicationController +class UiJobWizardController < Api::V2::BaseController include FiltersHelper def categories job_categories = resource_scope(permission: action_permission) diff --git a/webpack/JobWizard/JobWizardConstants.js b/webpack/JobWizard/JobWizardConstants.js index 7ef69b5f7..857728a5e 100644 --- a/webpack/JobWizard/JobWizardConstants.js +++ b/webpack/JobWizard/JobWizardConstants.js @@ -5,6 +5,10 @@ export const JOB_TEMPLATES = 'JOB_TEMPLATES'; export const JOB_CATEGORIES = 'JOB_CATEGORIES'; export const JOB_TEMPLATE = 'JOB_TEMPLATE'; export const JOB_INVOCATION = 'JOB_INVOCATION'; +export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS'; +export const currentPermissionsUrl = foremanUrl( + '/api/v2/permissions/current_permissions' +); export const templatesUrl = foremanUrl('/api/v2/job_templates'); export const repeatTypes = { diff --git a/webpack/JobWizard/JobWizardSelectors.js b/webpack/JobWizard/JobWizardSelectors.js index febdd82bb..0bc3738aa 100644 --- a/webpack/JobWizard/JobWizardSelectors.js +++ b/webpack/JobWizard/JobWizardSelectors.js @@ -1,4 +1,5 @@ import URI from 'urijs'; +import { get } from 'lodash'; import { selectAPIResponse, selectAPIStatus, @@ -42,6 +43,18 @@ export const selectJobCategoriesStatus = state => export const selectCategoryError = state => selectAPIErrorMessage(state, JOB_CATEGORIES); +export const selectJobCategoriesMissingPermissions = state => { + const jobCategoriesResponse = selectJobCategoriesResponse(state); + return ( + get(jobCategoriesResponse, [ + 'response', + 'data', + 'error', + 'missing_permissions', + ]) || [] + ); +}; + export const selectAllTemplatesError = state => selectAPIErrorMessage(state, JOB_TEMPLATES); @@ -60,11 +73,21 @@ export const selectAdvancedTemplateInputs = state => export const selectTemplateInputs = state => selectAPIResponse(state, JOB_TEMPLATE).template_inputs || []; +export const selectHostsResponse = state => selectAPIResponse(state, HOSTS_API); + export const selectHostCount = state => - selectAPIResponse(state, HOSTS_API).subtotal || 0; + selectHostsResponse(state).subtotal || 0; export const selectHosts = state => - (selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name); + (selectHostsResponse(state).results || []).map(host => host.name); + +export const selectHostsMissingPermissions = state => { + const hostsResponse = selectHostsResponse(state); + return ( + get(hostsResponse, ['response', 'data', 'error', 'missing_permissions']) || + [] + ); +}; export const selectIsLoadingHosts = state => !selectAPIStatus(state, HOSTS_API) || diff --git a/webpack/JobWizard/PermissionDenied.js b/webpack/JobWizard/PermissionDenied.js new file mode 100644 index 000000000..7f053cdde --- /dev/null +++ b/webpack/JobWizard/PermissionDenied.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { Icon } from 'patternfly-react'; +import { + Title, + Button, + EmptyState, + EmptyStateVariant, + EmptyStateBody, +} from '@patternfly/react-core'; + +const PermissionDenied = ({ missingPermissions, setProceedAnyway }) => { + const description = ( + + {__('You are not authorized to perform this action.')} +
+ {__( + 'Please request the required permissions listed below from a Foreman administrator:' + )} +
+ +
+ ); + const handleProceedAnyway = () => { + setProceedAnyway(true); + }; + + return ( + + + + + + {__('Permission Denied')} + + {description} + + + ); +}; + +PermissionDenied.propTypes = { + missingPermissions: PropTypes.array, + setProceedAnyway: PropTypes.func.isRequired, +}; + +PermissionDenied.defaultProps = { + missingPermissions: ['unknown'], +}; + +export default PermissionDenied; diff --git a/webpack/JobWizard/index.js b/webpack/JobWizard/index.js index 913754cd2..b1fdacd18 100644 --- a/webpack/JobWizard/index.js +++ b/webpack/JobWizard/index.js @@ -1,9 +1,17 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { isEmpty } from 'lodash'; import PropTypes from 'prop-types'; import { Title, Flex, FlexItem, Button } from '@patternfly/react-core'; import { translate as __ } from 'foremanReact/common/I18n'; +import { STATUS } from 'foremanReact/constants'; import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout'; +import { useAPI } from 'foremanReact/common/hooks/API/APIHooks'; +import PermissionDenied from './PermissionDenied'; import { JobWizard } from './JobWizard'; +import { + CURRENT_PERMISSIONS, + currentPermissionsUrl, +} from './JobWizardConstants'; const JobWizardPage = ({ location: { search } }) => { const title = __('Run job'); @@ -13,6 +21,37 @@ const JobWizardPage = ({ location: { search } }) => { { caption: title }, ], }; + const [proceedAnyway, setProceedAnyway] = useState(false); + + const { response, status } = useAPI( + 'get', + currentPermissionsUrl, + CURRENT_PERMISSIONS + ); + const desiredPermissions = [ + 'view_hosts', + 'view_smart_proxies', + 'view_job_templates', + 'create_job_invocations', + 'create_template_invocations', + ]; + const missingPermissions = + status === STATUS.RESOLVED + ? desiredPermissions.filter( + permission => + !response.results.map(result => result.name).includes(permission) + ) + : []; + + if (!isEmpty(missingPermissions) && !proceedAnyway) { + return ( + + ); + } + return ( @@ -69,7 +79,7 @@ export const CategoryAndTemplate = ({ options={jobCategories} setValue={onSelectCategory} value={selectedCategory} - placeholderText={categoryError ? __('Error') : ''} + placeholderText={categoryError ? __('Not available') : ''} isDisabled={!!categoryError} isRequired /> @@ -82,11 +92,22 @@ export const CategoryAndTemplate = ({ isDisabled={ !!(categoryError || allTemplatesError || isTemplatesLoading) } - placeholderText={allTemplatesError ? __('Error') : ''} + placeholderText={allTemplatesError ? __('Not available') : ''} /> + {!isEmpty(missingPermissions) && ( + + + {__( + `Missing the required permissions: ${missingPermissions.join( + ', ' + )}` + )} + + + )} {isError && ( - {categoryError && ( + {categoryError && isEmpty(missingPermissions) && ( {__('Categories list failed with:')} {categoryError} diff --git a/webpack/JobWizard/steps/HostsAndInputs/index.js b/webpack/JobWizard/steps/HostsAndInputs/index.js index 565a588d1..7f45c942a 100644 --- a/webpack/JobWizard/steps/HostsAndInputs/index.js +++ b/webpack/JobWizard/steps/HostsAndInputs/index.js @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; +import { isEmpty, debounce } from 'lodash'; import { + Alert, Button, Form, FormGroup, @@ -10,13 +12,13 @@ import { import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; import { FilterIcon } from '@patternfly/react-icons'; -import { debounce } from 'lodash'; import { get } from 'foremanReact/redux/API'; import { translate as __ } from 'foremanReact/common/I18n'; import { selectTemplateInputs, selectWithKatello, selectHostCount, + selectHostsMissingPermissions, selectIsLoadingHosts, } from '../../JobWizardSelectors'; import { SelectField } from '../form/SelectField'; @@ -98,6 +100,7 @@ const HostsAndInputs = ({ ]); const withKatello = useSelector(selectWithKatello); const hostCount = useSelector(selectHostCount); + const missingPermissions = useSelector(selectHostsMissingPermissions); const dispatch = useDispatch(); const selectedHosts = selected.hosts; @@ -126,6 +129,7 @@ const HostsAndInputs = ({ const [errorText, setErrorText] = useState( __('Please select at least one host') ); + return (
@@ -237,6 +241,17 @@ const HostsAndInputs = ({ value={templateValues} setValue={setTemplateValues} /> + {!isEmpty(missingPermissions) && ( + + + {__( + `Missing the required permissions: ${missingPermissions.join( + ', ' + )}` + )} + + + )}
);