Skip to content

Commit

Permalink
WIP update of Fixes #36785 - Show permission error page for job wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
kmalyjur committed Oct 25, 2023
1 parent 61c9c3e commit f0e9a9e
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 11 deletions.
2 changes: 1 addition & 1 deletion app/controllers/ui_job_wizard_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class UiJobWizardController < ApplicationController
class UiJobWizardController < Api::V2::BaseController
include FiltersHelper
def categories
job_categories = resource_scope(permission: action_permission)
Expand Down
5 changes: 4 additions & 1 deletion lib/foreman_remote_execution/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,21 @@ class Engine < ::Rails::Engine
:'api/v2/job_templates' => [:destroy] }, :resource_type => 'JobTemplate'
permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
:ui_job_wizard => [:categories],
'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host], :template_invocations => [:show],
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs] }, :resource_type => 'JobInvocation'
permission :view_template_invocations, { :template_invocations => [:show],
'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
permission :create_template_invocations, { :ui_job_wizard => [:categories] }, :resource_type => 'TemplateInvocation'
permission :execute_jobs_on_infrastructure_hosts, {}, :resource_type => 'JobInvocation'
permission :cancel_job_invocations, { :job_invocations => [:cancel], 'api/v2/job_invocations' => [:cancel] }, :resource_type => 'JobInvocation'
# this permissions grants user to get auto completion hints when setting up filters
permission :filter_autocompletion_for_template_invocation, { :template_invocations => [ :auto_complete_search, :index ] },
:resource_type => 'TemplateInvocation'
permission :cockpit_hosts, { 'cockpit' => [:redirect, :host_ssh_params] }, :resource_type => 'Host'
permission :view_hosts, { :ui_job_wizard => [:categories] }, :resource_type => 'Host'
permission :view_smart_proxies, { :ui_job_wizard => [:categories] }, :resource_type => 'SmartProxy'
end

USER_PERMISSIONS = [
Expand Down
27 changes: 25 additions & 2 deletions webpack/JobWizard/JobWizardSelectors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import URI from 'urijs';
import { get } from 'lodash';
import {
selectAPIResponse,
selectAPIStatus,
Expand Down Expand Up @@ -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);

Expand All @@ -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) ||
Expand Down
64 changes: 64 additions & 0 deletions webpack/JobWizard/PermissionDenied.js
Original file line number Diff line number Diff line change
@@ -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 = (
<span>
{__('You are not authorized to perform this action.')}
<br />
{__(
'Please request the required permissions listed below from a Foreman administrator:'
)}
<br />
<ul className="list-unstyled">
{missingPermissions.map(permission => (
<li key={permission}>
<strong>{permission}</strong>
</li>
))}
</ul>
</span>
);
const handleProceedAnyway = () => {
setProceedAnyway(true);
};

return (
<EmptyState variant={EmptyStateVariant.xl}>
<span className="empty-state-icon">
<Icon name="lock" type="fa" size="2x" />
</span>
<Title ouiaId="empty-state-header" headingLevel="h5" size="4xl">
{__('Permission Denied')}
</Title>
<EmptyStateBody>{description}</EmptyStateBody>
<Button
ouiaId="job-invocation-proceed-anyway-button"
variant="primary"
onClick={handleProceedAnyway}
>
{__('Proceed Anyway')}
</Button>
</EmptyState>
);
};

PermissionDenied.propTypes = {
missingPermissions: PropTypes.array,
setProceedAnyway: PropTypes.func.isRequired,
};

PermissionDenied.defaultProps = {
missingPermissions: ['unknown'],
};

export default PermissionDenied;
25 changes: 24 additions & 1 deletion webpack/JobWizard/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import React from 'react';
import React, { useState } from 'react';
import { isEmpty } from 'lodash';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Title, Flex, FlexItem, Button } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
import {
selectJobCategoriesMissingPermissions,
selectIsLoading,
} from './JobWizardSelectors';
import PermissionDenied from './PermissionDenied';
import { JobWizard } from './JobWizard';
import { JOB_CATEGORIES } from './JobWizardConstants';

const JobWizardPage = ({ location: { search } }) => {
const title = __('Run job');
Expand All @@ -13,6 +21,21 @@ const JobWizardPage = ({ location: { search } }) => {
{ caption: title },
],
};
const missingPermissions = useSelector(selectJobCategoriesMissingPermissions);
const isCategoriesLoading = useSelector(state =>
selectIsLoading(state, JOB_CATEGORIES)
);
const [proceedAnyway, setProceedAnyway] = useState(false);

if (!isEmpty(missingPermissions) && !isCategoriesLoading && !proceedAnyway) {
return (
<PermissionDenied
missingPermissions={missingPermissions}
setProceedAnyway={setProceedAnyway}
/>
);
}

return (
<PageLayout
header={title}
Expand Down
28 changes: 23 additions & 5 deletions webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import {
selectJobCategoriesMissingPermissions,
selectIsLoading,
} from '../../JobWizardSelectors';
import { SelectField } from '../form/SelectField';
import { GroupedSelectField } from '../form/GroupedSelectField';
import { WizardTitle } from '../form/WizardTitle';
import { WIZARD_TITLES, JOB_TEMPLATES } from '../../JobWizardConstants';
import { selectIsLoading } from '../../JobWizardSelectors';

export const CategoryAndTemplate = ({
jobCategories,
Expand Down Expand Up @@ -57,7 +60,15 @@ export const CategoryAndTemplate = ({
};

const { categoryError, allTemplatesError, templateError } = errors;
const isError = !!(categoryError || allTemplatesError || templateError);
const missingPermission = useSelector(
selectJobCategoriesMissingPermissions
).includes('view_job_templates');
const isError = !!(
(categoryError && !missingPermission) ||
allTemplatesError ||
templateError
);

return (
<>
<WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
Expand All @@ -69,7 +80,7 @@ export const CategoryAndTemplate = ({
options={jobCategories}
setValue={onSelectCategory}
value={selectedCategory}
placeholderText={categoryError ? __('Error') : ''}
placeholderText={categoryError ? __('Not available') : ''}
isDisabled={!!categoryError}
isRequired
/>
Expand All @@ -82,11 +93,18 @@ export const CategoryAndTemplate = ({
isDisabled={
!!(categoryError || allTemplatesError || isTemplatesLoading)
}
placeholderText={allTemplatesError ? __('Error') : ''}
placeholderText={allTemplatesError ? __('Not available') : ''}
/>
{missingPermission && (
<Alert variant="warning" title={__('Access denied')}>
<span>
{__('Missing the required permissions: view_job_templates')}
</span>
</Alert>
)}
{isError && (
<Alert variant="danger" title={__('Errors:')}>
{categoryError && (
{categoryError && !missingPermission && (
<span>
{__('Categories list failed with:')} {categoryError}
</span>
Expand Down
13 changes: 12 additions & 1 deletion webpack/JobWizard/steps/HostsAndInputs/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react';
import { isEmpty, debounce } from 'lodash';
import {
Alert,
Button,
Form,
FormGroup,
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -126,6 +129,7 @@ const HostsAndInputs = ({
const [errorText, setErrorText] = useState(
__('Please select at least one host')
);

return (
<div className="target-hosts-and-inputs">
<WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
Expand Down Expand Up @@ -237,6 +241,13 @@ const HostsAndInputs = ({
value={templateValues}
setValue={setTemplateValues}
/>
{!isEmpty(missingPermissions) && (
<Alert variant="warning" title={__('Access denied')}>
<span>
{__(`Missing the required permissions:`)} {missingPermissions}
</span>
</Alert>
)}
</Form>
</div>
);
Expand Down

0 comments on commit f0e9a9e

Please sign in to comment.