diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 261600110..a5cab3353 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -503,6 +503,8 @@ message.delete-group-body=You are requesting to delete a group. If you complete message.delete-attribute-title=Delete Attribute? message.delete-attribute-body=You are requesting to delete a custom attribute. If you complete this process the attribute will be removed. This cannot be undone. Do you wish to continue? +message.group-pattern-fail=Pattern must match group url validation pattern. + message.must-be-unique=Must be unique. message.must-be-number=Must be a number. message.name-must-be-unique=Name must be unique. diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 483f1edeb..f0a768cdc 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -22,9 +22,7 @@ export function useGroupUiSchema () { } export function useGroupUiValidator() { - console.log('hi') return (formData, errors) => { - console.log(formData, errors) if (!isNil(formData?.validationRegex)) { const isValid = isValidRegex(formData.validationRegex); if (!isValid) { diff --git a/ui/src/app/admin/hooks.test.js b/ui/src/app/admin/hooks.test.js new file mode 100644 index 000000000..303540317 --- /dev/null +++ b/ui/src/app/admin/hooks.test.js @@ -0,0 +1,9 @@ +import { useGroupUiValidator } from './hooks'; + +it('should validate against a regex', () => { + const validator = useGroupUiValidator(); + const addErrorSpy = jest.fn(); + const fail = validator({ validationRegex: '))(()' }, { validationRegex: { addError: addErrorSpy } }); + expect(addErrorSpy).toHaveBeenCalled(); + expect(validator({validationRegex: '/*'})).toBeUndefined(); +}); \ No newline at end of file diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index d656ac4a4..064c1e65f 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -7,23 +7,33 @@ const UserContext = React.createContext(); const { Provider, Consumer } = UserContext; const path = '/admin/users/current'; +const group = { + "name": "ADMIN-GROUP", + "resourceId": "admingroup", + "validationRegex": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$", + "ownerType": "GROUP", + "ownerId": "admingroup" +}; /*eslint-disable react-hooks/exhaustive-deps*/ function UserProvider({ children }) { const { get, response } = useFetch(`${API_BASE_PATH}`, { - cacheLife: 10000, - cachePolicy: 'cache-first' + cachePolicy: 'no-cache' }); React.useEffect(() => { loadUser() }, []); async function loadUser() { const user = await get(`${path}`); - if (response.ok) setUser(user); + if (response.ok) setUser({ + ...user, + group + }); } const [user, setUser] = React.useState({}); + return ( {children} ); @@ -50,5 +60,24 @@ function useIsAdminOrInGroup() { return isAdmin || isInGroup; } +function useUserGroup() { + const user = useCurrentUser(); + return user?.group; +} + +function useUserGroupRegexValidator () { + const user = useCurrentUser(); + return user?.group?.validationRegex; +} + -export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin, useIsAdminOrInGroup }; \ No newline at end of file +export { + UserContext, + UserProvider, + Consumer as UserConsumer, + useCurrentUser, + useIsAdmin, + useIsAdminOrInGroup, + useUserGroupRegexValidator, + useUserGroup +}; \ No newline at end of file diff --git a/ui/src/app/form/component/fields/FilterTargetField.js b/ui/src/app/form/component/fields/FilterTargetField.js index 959306b71..9aaf84b97 100644 --- a/ui/src/app/form/component/fields/FilterTargetField.js +++ b/ui/src/app/form/component/fields/FilterTargetField.js @@ -45,8 +45,13 @@ const FilterTargetField = ({ onChange, errorSchema, formData, + formContext, ...props }) => { + + const { group } = formContext; + const regex = new RegExp(group?.validationRegex || '/*'); + const typeFieldName = `${name}Type`; const type = schema.properties[typeFieldName]; @@ -59,6 +64,8 @@ const FilterTargetField = ({ const [selectedTarget, setSelectedTarget] = React.useState([...(formData.value && !isNil(formData.value) && !isNil(formData.value[0]) ? formData.value : [])]); const [term, setSearchTerm] = React.useState(''); + const [match, setMatch] = React.useState(true); + const [touched, setTouched] = React.useState(false); const [ids, setSearchIds] = React.useState([]); const { get, response } = useFetch(`${API_BASE_PATH}/EntityIds/search`, { @@ -113,6 +120,7 @@ const FilterTargetField = ({ const onEntityIdsChange = (value) => { setSearchTerm(value); + setMatch(regex ? regex.test(value) : true); }; const selectType = (option) => { @@ -172,19 +180,28 @@ const FilterTargetField = ({ onInputChange={onEntityIdsChange} selected={ [term] } onChange={ () => {} } + onBlur={() => setTouched(true)} onSearch={ (query) => setSearchTerm(query) } - renderMenuItemChildren={(option, { options, text }, index) => { - return {option}; - }}> + renderMenuItemChildren={(option, { options, text }, index) => + {option} + }> {({ isMenuShown, toggleMenu }) => ( toggleMenu()} disabled={disabled || readonly} /> )} - - - You must add at least one entity id target and they must each be unique. - - + {(!touched || match) ? + + + You must add at least one entity id target and they must each be unique. + + + : + + + Invalid URL + + + } > } { targetType === 'CONDITION_SCRIPT' && @@ -232,7 +249,7 @@ const FilterTargetField = ({ onSelectValue(term)}> Add Entity ID diff --git a/ui/src/app/metadata/domain/filter/definition/BaseFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/BaseFilterDefinition.js index 16d2cf7fc..8bdf9bfab 100644 --- a/ui/src/app/metadata/domain/filter/definition/BaseFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/BaseFilterDefinition.js @@ -1,7 +1,39 @@ +import { isValidRegex } from '../../../../core/utility/is_valid_regex'; + export const BaseFilterDefinition = { parser: (changes) => changes, formatter: (changes) => changes, display: (changes) => changes, + validator: (data = [], current = { resourceId: null }, group, targetProp, typeProp) => { + + const filters = current ? data.filter(s => s.resourceId !== current.resourceId) : data; + const names = filters.map(s => s.name); + + return (formData, errors) => { + if (names.indexOf(formData.name) > -1) { + errors.name.addError('message.name-unique'); + } + + if (formData.hasOwnProperty(targetProp)) { + if (formData[targetProp][typeProp] === 'REGEX') { + const { [targetProp]: { value } } = formData; + const isValid = isValidRegex(value[0]); + if (!isValid) { + errors[targetProp].value.addError('message.invalid-regex-pattern'); + } + } + + if (formData[targetProp][typeProp] === 'CONDITION_SCRIPT') { + const { [targetProp]: { value } } = formData; + if (!value[0]) { + errors[targetProp].value.addError('message.required-for-scripts'); + } + } + } + + return errors; + } + }, uiSchema: { '@type': { 'ui:widget': 'hidden' diff --git a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js index aab3adb4d..8ece90ecc 100644 --- a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js @@ -1,7 +1,6 @@ import API_BASE_PATH from "../../../../App.constant"; import {BaseFilterDefinition} from './BaseFilterDefinition'; import {removeNull} from '../../../../core/utility/remove_null'; -import { isValidRegex } from '../../../../core/utility/is_valid_regex'; import defaultsDeep from "lodash/defaultsDeep"; export const EntityAttributesFilterWizard = { @@ -36,32 +35,8 @@ export const EntityAttributesFilterWizard = { } } }, BaseFilterDefinition.uiSchema), - validator: (data = [], current = { resourceId: null }) => { - - const filters = current ? data.filter(s => s.resourceId !== current.resourceId) : data; - const names = filters.map(s => s.name); - - return (formData, errors) => { - if (names.indexOf(formData.name) > -1) { - errors.name.addError('message.name-unique'); - } - - if (formData?.entityAttributesFilterTarget?.entityAttributesFilterTargetType === 'REGEX') { - const { entityAttributesFilterTarget: {value} } = formData; - const isValid = isValidRegex(value[0]); - if (!isValid) { - errors.entityAttributesFilterTarget.value.addError('message.invalid-regex-pattern'); - } - } - - if (formData?.entityAttributesFilterTarget?.entityAttributesFilterTargetType === 'CONDITION_SCRIPT') { - const { entityAttributesFilterTarget: { value } } = formData; - if (!value[0]) { - errors.entityAttributesFilterTarget.value.addError('message.required-for-scripts'); - } - } - return errors; - } + validator: (data = [], current = { resourceId: null }, group) => { + return BaseFilterDefinition.validator(data, current, group, 'entityAttributesFilterTarget', 'entityAttributesFilterTargetType') }, warnings: (data) => { let warnings = {}; diff --git a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js index e4abd17eb..f05ecd703 100644 --- a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js @@ -2,8 +2,6 @@ import defaultsDeep from "lodash/defaultsDeep"; import API_BASE_PATH from "../../../../App.constant"; import { BaseFilterDefinition } from "./BaseFilterDefinition"; -import { isValidRegex } from '../../../../core/utility/is_valid_regex'; - export const NameIDFilterWizard = { ...BaseFilterDefinition, uiSchema: defaultsDeep({ @@ -24,32 +22,8 @@ export const NameIDFilterWizard = { type: 'NameIDFormat', schema: `${API_BASE_PATH}/ui/NameIdFormatFilter`, steps: [], - validator: (data = [], current = { resourceId: null }) => { - - const filters = current ? data.filter(s => s.resourceId !== current.resourceId) : data; - const names = filters.map(s => s.name); - - return (formData, errors) => { - if (names.indexOf(formData.name) > -1) { - errors.name.addError('message.name-unique'); - } - - if (formData?.nameIdFormatFilterTarget?.nameIdFormatFilterTargetType === 'REGEX') { - const { nameIdFormatFilterTarget: { value } } = formData; - const isValid = isValidRegex(value[0]); - if (!isValid) { - errors.nameIdFormatFilterTarget.value.addError('message.invalid-regex-pattern'); - } - } - - if (formData?.nameIdFormatFilterTarget?.nameIdFormatFilterTargetType === 'CONDITION_SCRIPT') { - const { nameIdFormatFilterTarget: { value } } = formData; - if (!value[0]) { - errors.nameIdFormatFilterTarget.value.addError('message.required-for-scripts'); - } - } - return errors; - } + validator: (data = [], current = { resourceId: null }, group) => { + return BaseFilterDefinition.validator(data, current, group, 'nameIdFormatFilterTarget', 'nameIdFormatFilterTargetType') }, formatter: (changes) => ({ ...changes, diff --git a/ui/src/app/metadata/domain/provider/definition/BaseProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/BaseProviderDefinition.js index c801ddb04..93f8efa71 100644 --- a/ui/src/app/metadata/domain/provider/definition/BaseProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/BaseProviderDefinition.js @@ -4,7 +4,7 @@ import { MetadataFilterTypes } from '../../filter'; export const BaseProviderDefinition = { schemaPreprocessor: metadataFilterProcessor, - validator: (data = [], current = { resourceId: null }) => { + validator: (data = [], current = { resourceId: null }, group) => { const providers = data.filter(p => p.resourceId !== current.resourceId); const names = providers.map(s => s.name); const ids = providers.map(s => s.xmlId); diff --git a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js index 87ed3bb31..af15a7c02 100644 --- a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js @@ -9,8 +9,11 @@ export const DynamicHttpMetadataProviderWizard = { label: 'DynamicHttpMetadataProvider', type: 'DynamicHttpMetadataResolver', schema: `${API_BASE_PATH}/ui/MetadataResolver/DynamicHttpMetadataResolver`, - validator: (data = [], current = { resourceId: null }) => { - const base = BaseProviderDefinition.validator(data, current); + validator: (data = [], current = { resourceId: null }, group) => { + const base = BaseProviderDefinition.validator(data, current, group); + + const pattern = group?.validationRegex ? new RegExp(group?.validationRegex) : null; + return (formData, errors) => { const errorList = base(formData, errors); if (formData?.metadataRequestURLConstructionScheme['@type'] === 'Regex') { @@ -21,6 +24,12 @@ export const DynamicHttpMetadataProviderWizard = { } } + if (formData?.metadataRequestURLConstructionScheme['@type'] === 'MetadataQueryProtocol') { + if (pattern && !pattern.test(formData?.metadataRequestURLConstructionScheme?.content)) { + errors?.metadataRequestURLConstructionScheme?.content?.addError('message.group-pattern-fail'); + } + } + return errorList; } }, diff --git a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js index 5d2d8e009..2c37d52c3 100644 --- a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js @@ -8,6 +8,22 @@ export const FileBackedHttpMetadataProviderWizard = { label: 'FileBackedHttpMetadataProvider', type: 'FileBackedHttpMetadataResolver', schema: '/assets/schema/provider/filebacked-http.schema.json', + validator: (data = [], current = { resourceId: null }, group) => { + const base = BaseProviderDefinition.validator(data, current, group); + + const pattern = group?.validationRegex ? new RegExp(group?.validationRegex) : null; + + return (formData, errors) => { + const errorList = base(formData, errors); + if (formData?.metadataURL) { + if (pattern && !pattern.test(formData?.metadataURL)) { + errors?.metadataURL?.addError('message.group-pattern-fail'); + } + } + + return errorList; + } + }, steps: [ ...BaseProviderDefinition.steps, { diff --git a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js index bc11108ee..fcabbd1d0 100644 --- a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js @@ -5,6 +5,7 @@ import defaultsDeep from 'lodash/defaultsDeep'; import API_BASE_PATH from '../../../../App.constant'; import {removeNull} from '../../../../core/utility/remove_null'; import { detailedDiff } from 'deep-object-diff'; +import isNil from 'lodash/isNil'; export const SourceBase = { label: 'Metadata Source', @@ -18,20 +19,33 @@ export const SourceBase = { display: (changes) => changes, - validator: (data = [], current = {id: null}) => { + validator: (data = [], current = {id: null}, group) => { const sources = current ? data.filter(s => s.id !== current.id) : data; const entityIds = sources.map(s => s.entityId); + const pattern = group?.validationRegex ? new RegExp(group?.validationRegex) : null; return (formData, errors) => { if (entityIds.indexOf(formData.entityId) > -1) { errors.entityId.addError('message.id-unique'); } + if (pattern && !pattern.test(formData.entityId)) { + errors.entityId.addError('message.group-pattern-fail'); + } + if (formData?.serviceProviderSsoDescriptor?.nameIdFormats?.length > 0 && !formData.serviceProviderSsoDescriptor.protocolSupportEnum) { errors.serviceProviderSsoDescriptor.protocolSupportEnum.addError('message.protocol-support-required') } + if (Array.isArray(formData?.assertionConsumerServices)) { + formData.assertionConsumerServices.forEach((acs, idx) => { + if (!isNil(acs?.locationUrl) && !pattern.test(acs.locationUrl)) { + errors.assertionConsumerServices[idx].locationUrl.addError('message.group-pattern-fail') + } + }); + } + return errors; } }, diff --git a/ui/src/app/metadata/editor/MetadataEditor.js b/ui/src/app/metadata/editor/MetadataEditor.js index 31bc30da0..b8ee9f14a 100644 --- a/ui/src/app/metadata/editor/MetadataEditor.js +++ b/ui/src/app/metadata/editor/MetadataEditor.js @@ -19,10 +19,12 @@ import { MetadataObjectContext } from '../hoc/MetadataSelector'; import { FilterableProviders } from '../domain/provider'; import { checkChanges } from '../hooks/utility'; import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useUserGroup } from '../../core/user/UserContext'; export function MetadataEditor ({ current }) { const translator = useTranslator(); + const group = useUserGroup(); const { type, id, section } = useParams(); @@ -77,7 +79,7 @@ export function MetadataEditor ({ current }) { const [blocking, setBlocking] = React.useState(false); - const validator = definition.validator(data, current); + const validator = definition.validator(data, current, group); const warnings = definition.warnings && definition.warnings(metadata); diff --git a/ui/src/app/metadata/editor/MetadataEditorForm.js b/ui/src/app/metadata/editor/MetadataEditorForm.js index cf38c108a..cbca81310 100644 --- a/ui/src/app/metadata/editor/MetadataEditorForm.js +++ b/ui/src/app/metadata/editor/MetadataEditorForm.js @@ -9,6 +9,7 @@ import { useUiSchema } from '../hooks/schema'; import Alert from 'react-bootstrap/Alert'; import { transformErrors } from '../domain/transform'; +import { useUserGroup } from '../../core/user/UserContext'; function ErrorListTemplate () { return (<>>); @@ -30,6 +31,12 @@ export function MetadataEditorForm({ metadata, definition, schema, current, onCh onChange(definition.bindings ? { ...form, formData: definition.bindings(data, form.formData) }: form); }; + const group = useUserGroup(); + + const context = { + group + }; + return ( <> {step.locked && @@ -63,7 +70,8 @@ export function MetadataEditorForm({ metadata, definition, schema, current, onCh liveValidate={true} transformErrors={transformErrors} ErrorList={ErrorListTemplate} - validate={validator}> + validate={validator} + formContext={context}> <>> diff --git a/ui/src/app/metadata/editor/MetadataFilterEditor.js b/ui/src/app/metadata/editor/MetadataFilterEditor.js index 85e41755e..44e8f9cba 100644 --- a/ui/src/app/metadata/editor/MetadataFilterEditor.js +++ b/ui/src/app/metadata/editor/MetadataFilterEditor.js @@ -13,10 +13,12 @@ import { MetadataEditorNav } from './MetadataEditorNav'; import { useMetadataFilters } from '../hooks/api'; import { MetadataFilterContext } from '../hoc/MetadataFilterSelector'; import { checkChanges } from '../hooks/utility'; +import { useUserGroup } from '../../core/user/UserContext'; export function MetadataFilterEditor({children, onNavigate, block}) { const { id, section } = useParams(); + const group = useUserGroup(); const { data } = useMetadataFilters(id, {}, []); const definition = React.useContext(MetadataDefinitionContext); @@ -32,7 +34,7 @@ export function MetadataFilterEditor({children, onNavigate, block}) { block(checkChanges(metadata, changes.formData)); }; - const validator = definition.validator(data, current); + const validator = definition.validator(data, current, group); const warnings = definition.warnings && definition.warnings(metadata); diff --git a/ui/src/app/metadata/wizard/MetadataProviderWizard.js b/ui/src/app/metadata/wizard/MetadataProviderWizard.js index 37022bd13..bbcca848b 100644 --- a/ui/src/app/metadata/wizard/MetadataProviderWizard.js +++ b/ui/src/app/metadata/wizard/MetadataProviderWizard.js @@ -12,10 +12,12 @@ import { Prompt, useHistory } from 'react-router'; import { removeNull } from '../../core/utility/remove_null'; import { useNotificationDispatcher, createNotificationAction, NotificationTypes } from '../../notifications/hoc/Notifications'; +import { useUserGroup } from '../../core/user/UserContext'; export function MetadataProviderWizard({onRestart}) { const { data } = useMetadataProviders({cachePolicy: 'no-cache'}, []); + const group = useUserGroup(); const { post, loading, response } = useMetadataEntity('provider'); const history = useHistory(); @@ -51,7 +53,7 @@ export function MetadataProviderWizard({onRestart}) { // console.log(form); } - const validator = definition.validator(data); + const validator = definition.validator(data, null, group); async function save() { const body = removeNull(definition.parser(metadata), true); diff --git a/ui/src/app/metadata/wizard/MetadataSourceWizard.js b/ui/src/app/metadata/wizard/MetadataSourceWizard.js index 0d23b34af..6da398350 100644 --- a/ui/src/app/metadata/wizard/MetadataSourceWizard.js +++ b/ui/src/app/metadata/wizard/MetadataSourceWizard.js @@ -18,12 +18,14 @@ import { removeNull } from '../../core/utility/remove_null'; import Translate from '../../i18n/components/translate'; import { checkChanges } from '../hooks/utility'; +import { useUserGroup } from '../../core/user/UserContext'; export function MetadataSourceWizard ({ onShowNav }) { const { post, loading, response } = useMetadataEntity('source'); const history = useHistory(); + const group = useUserGroup(); const { data } = useMetadataSources({ cachePolicy: 'no-cache' @@ -72,7 +74,7 @@ export function MetadataSourceWizard ({ onShowNav }) { const [blocking, setBlocking] = React.useState(false); - const validator = definition.validator(data); + const validator = definition.validator(data, null, group); const warnings = definition.warnings && definition.warnings(metadata); return (