diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties
index 673a47728..5640fd5de 100644
--- a/backend/src/main/resources/i18n/messages.properties
+++ b/backend/src/main/resources/i18n/messages.properties
@@ -477,6 +477,8 @@ message.no-filters-added=No filters have been added to this Metadata Provider
message.create-new-version-from-version=Create New Version from Previous Settings
message.restoring-this-version-will-copy=Restoring this version will copy the Version ({ date }) configuration and create a new Version from the selected version settings. You can then edit the configuration before saving the new version.
+message.invalid-regex-pattern=Invalid Regular Expression
+
tooltip.entity-id=Entity ID
tooltip.service-provider-name=Service Provider Name (Dashboard Display Only)
tooltip.force-authn=Disallows use (or reuse) of authentication results and login flows that don\u0027t provide a real-time proof of user presence in the login process
diff --git a/ui/src/app/metadata/configuration/component/property/array-property.component.html b/ui/src/app/metadata/configuration/component/property/array-property.component.html
index 9a5d1648e..380e0105b 100644
--- a/ui/src/app/metadata/configuration/component/property/array-property.component.html
+++ b/ui/src/app/metadata/configuration/component/property/array-property.component.html
@@ -20,6 +20,20 @@
+
+
+ {{ version[i][prop] }}
+
+
+ -
+
+
diff --git a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts
index 22eae4cda..bb544cf0d 100644
--- a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts
+++ b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts
@@ -14,8 +14,8 @@ describe('EntityAttributesFilter Entity', () => {
expect(entity.resourceId).toBe('foo');
expect(entity.enabled).toBe(entity.filterEnabled);
expect(entity.id).toBe(entity.resourceId);
- expect(entity.getId()).toBe(entity.entityId);
- expect(entity.getDisplayId()).toBe(entity.entityId);
+ expect(entity.getId()).toBe(entity.resourceId);
+ expect(entity.getDisplayId()).toBe(entity.resourceId);
expect(entity.isDraft()).toBe(false);
});
});
diff --git a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts
index 839324879..f536a5b38 100644
--- a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts
+++ b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts
@@ -31,11 +31,11 @@ export class EntityAttributesFilterEntity implements MetadataFilter, MetadataEnt
}
getId(): string {
- return this.entityId;
+ return this.resourceId;
}
getDisplayId(): string {
- return this.entityId;
+ return this.resourceId;
}
isDraft(): boolean {
diff --git a/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.spec.ts b/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.spec.ts
new file mode 100644
index 000000000..ba39ffbe2
--- /dev/null
+++ b/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.spec.ts
@@ -0,0 +1,21 @@
+import { NameIDFormatFilterEntity } from './nameid-format-filter';
+
+describe('NameIDFormatFilterEntity Entity', () => {
+ let entity: NameIDFormatFilterEntity;
+ beforeEach(() => {
+ entity = new NameIDFormatFilterEntity({
+ resourceId: 'foo',
+ filterEnabled: false
+ });
+ });
+
+ it('should be an instance', () => {
+ expect(entity).toBeDefined();
+ expect(entity.resourceId).toBe('foo');
+ expect(entity.enabled).toBe(entity.filterEnabled);
+ expect(entity.id).toBe(entity.resourceId);
+ expect(entity.getId()).toBe(entity.resourceId);
+ expect(entity.getDisplayId()).toBe(entity.resourceId);
+ expect(entity.isDraft()).toBe(false);
+ });
+});
diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts
index 53e3630c8..a5505284d 100644
--- a/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts
+++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts
@@ -5,7 +5,8 @@ describe('Entity Attributes filter form', () => {
it('should return an empty object for validators', () => {
expect(Object.keys(EntityAttributesFilter.getValidators())).toEqual([
'/',
- '/name'
+ '/name',
+ '/entityAttributesFilterTarget'
]);
});
diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts
index a2642ddc9..f1d58d915 100644
--- a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts
+++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts
@@ -2,7 +2,11 @@ import { FormDefinition } from '../../../wizard/model';
import { MetadataFilter } from '../../domain/model';
import { removeNulls } from '../../../shared/util';
import { EntityAttributesFilterEntity } from '../../domain/entity';
+import { RegexValidator } from '../../../shared/validation/regex.validator';
import { getFilterNames } from '../reducer';
+import { memoize } from '../../../shared/memo';
+
+const checkRegex = memoize(RegexValidator.isValidRegex);
export const EntityAttributesFilter: FormDefinition = {
label: 'EntityAttributes',
@@ -37,6 +41,18 @@ export const EntityAttributesFilter: FormDefinition = {
params: [value]
} : null;
return err;
+ },
+ '/entityAttributesFilterTarget': (value, property, form) => {
+ if (!form || !form.value || !form.value.entityAttributesFilterTarget ||
+ form.value.entityAttributesFilterTarget.entityAttributesFilterTargetType !== 'REGEX') {
+ return null;
+ }
+ return checkRegex(value.value[0]) ? null : {
+ code: 'INVALID_REGEX',
+ path: `#${property.path}`,
+ message: 'message.invalid-regex-pattern',
+ params: [value.value[0]]
+ };
}
};
return validators;
diff --git a/ui/src/app/metadata/filter/model/nameid.filter.spec.ts b/ui/src/app/metadata/filter/model/nameid.filter.spec.ts
index f366ca90a..e3d3aca36 100644
--- a/ui/src/app/metadata/filter/model/nameid.filter.spec.ts
+++ b/ui/src/app/metadata/filter/model/nameid.filter.spec.ts
@@ -5,7 +5,8 @@ describe('NameID Format filter form', () => {
it('should return an empty object for validators', () => {
expect(Object.keys(NameIDFilter.getValidators())).toEqual([
'/',
- '/name'
+ '/name',
+ '/nameIdFormatFilterTarget'
]);
});
diff --git a/ui/src/app/metadata/filter/model/nameid.filter.ts b/ui/src/app/metadata/filter/model/nameid.filter.ts
index 9751d4ce9..ee48f4f89 100644
--- a/ui/src/app/metadata/filter/model/nameid.filter.ts
+++ b/ui/src/app/metadata/filter/model/nameid.filter.ts
@@ -1,7 +1,11 @@
import { FormDefinition } from '../../../wizard/model';
import { MetadataFilter } from '../../domain/model';
import { NameIDFormatFilterEntity } from '../../domain/entity/filter/nameid-format-filter';
+import { RegexValidator } from '../../../shared/validation/regex.validator';
import { getFilterNames } from '../reducer';
+import { memoize } from '../../../shared/memo';
+
+const checkRegex = memoize(RegexValidator.isValidRegex);
export const NameIDFilter: FormDefinition = {
label: 'NameIDFormat',
@@ -36,6 +40,18 @@ export const NameIDFilter: FormDefinition = {
params: [value]
} : null;
return err;
+ },
+ '/nameIdFormatFilterTarget': (value, property, form) => {
+ if (!form || !form.value || !form.value.nameIdFormatFilterTarget ||
+ form.value.nameIdFormatFilterTarget.nameIdFormatFilterTargetType !== 'REGEX') {
+ return null;
+ }
+ return checkRegex(value.value[0]) ? null : {
+ code: 'INVALID_REGEX',
+ path: `#${property.path}`,
+ message: 'message.invalid-regex-pattern',
+ params: [value.value[0]]
+ };
}
};
return validators;
diff --git a/ui/src/app/schema-form/widget/array/array.component.ts b/ui/src/app/schema-form/widget/array/array.component.ts
index 18f93566e..82a089635 100644
--- a/ui/src/app/schema-form/widget/array/array.component.ts
+++ b/ui/src/app/schema-form/widget/array/array.component.ts
@@ -9,9 +9,9 @@ export interface FormError {
code: string;
description: string;
message: string;
- params: any[];
+ params?: any[];
path: string;
- schemaId: any;
+ schemaId?: any;
}
@Component({
diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.html b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
index ef3a7be4a..fcbd80ab8 100644
--- a/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
+++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
@@ -69,11 +69,21 @@
-
-
+
+
Required for Regex
+
+
+ ,
+ {{ error.message }}
+
+
diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts b/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts
index 4107ebfd3..7af0c8b0d 100644
--- a/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts
+++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts
@@ -2,8 +2,8 @@ import { Component, OnDestroy, AfterViewInit } from '@angular/core';
import { FormControl, Validators, AbstractControl, ValidatorFn } from '@angular/forms';
import { ObjectWidget } from 'ngx-schema-form';
import { Store } from '@ngrx/store';
-import { Observable } from 'rxjs';
-import { distinctUntilChanged, skipWhile, map } from 'rxjs/operators';
+import { Observable, Subject } from 'rxjs';
+import { distinctUntilChanged, skipWhile, takeUntil, map } from 'rxjs/operators';
import * as fromRoot from '../../../app.reducer';
import * as fromFilters from '../../../metadata/filter/reducer';
@@ -17,7 +17,7 @@ import { QueryEntityIds, ClearSearch } from '../../../metadata/filter/action/sea
styleUrls: ['./filter-target.component.scss']
})
export class FilterTargetComponent extends ObjectWidget implements OnDestroy, AfterViewInit {
-
+ private ngUnsubscribe: Subject
= new Subject();
ids$: Observable;
ids: string[];
@@ -32,6 +32,9 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
[Validators.required]
);
+ errors$: Observable;
+ hasErrors$: Observable;
+
constructor(
private store: Store
) {
@@ -42,6 +45,7 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
this.search
.valueChanges
.pipe(
+ takeUntil(this.ngUnsubscribe),
distinctUntilChanged()
)
.subscribe(query => this.searchEntityIds(query));
@@ -49,6 +53,7 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
this.script
.valueChanges
.pipe(
+ takeUntil(this.ngUnsubscribe),
distinctUntilChanged(),
skipWhile(() => this.targetType === 'ENTITY')
)
@@ -61,6 +66,31 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
super.ngAfterViewInit();
this.script.setValue(this.targets[0]);
this.search.setValidators(this.unique());
+
+ this.errors$ = this.formProperty.errorsChanges.pipe(
+ map(errors =>
+ errors && errors.length > 1 ?
+ Array
+ .from(new Set(errors.filter(e => e.code !== 'ARRAY_LENGTH_SHORT').map(e => e.code)))
+ .map(id => ({ ...errors.find(e => e.code === id) }))
+ : []
+ ));
+
+ this.errors$
+ .pipe(
+ takeUntil(this.ngUnsubscribe),
+ map(errors => errors.reduce((collection, e) => ({ ...collection, [e.code]: e.message }), {})),
+ map(errors => Object.keys(errors).length > 0 ? errors : null)
+ )
+ .subscribe(errors => this.script.setErrors(
+ errors,
+ {
+ emitEvent: true
+ }
+ )
+ );
+
+ this.hasErrors$ = this.errors$.pipe(map(e => e && e.length > 0));
}
unique(): ValidatorFn {
@@ -138,5 +168,7 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
ngOnDestroy(): void {
this.store.dispatch(new ClearSearch());
+ this.ngUnsubscribe.next();
+ this.ngUnsubscribe.complete();
}
}
diff --git a/ui/src/app/shared/memo.spec.ts b/ui/src/app/shared/memo.spec.ts
new file mode 100644
index 000000000..01fcd9898
--- /dev/null
+++ b/ui/src/app/shared/memo.spec.ts
@@ -0,0 +1,21 @@
+import { memoize } from './memo';
+
+const fns = {
+ square(n) {
+ return n * n;
+ }
+};
+
+describe('memoize function', () => {
+ it('should return a memoized function', () => {
+ spyOn(fns, 'square').and.callThrough();
+ const memoized = memoize(fns.square);
+ const call1 = memoized(1);
+ const call2 = memoized(2);
+ const call3 = memoized(2);
+ expect(call1).toBe(1);
+ expect(call2).toBe(4);
+ expect(call3).toBe(4);
+ expect(fns.square).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/ui/src/app/shared/memo.ts b/ui/src/app/shared/memo.ts
new file mode 100644
index 000000000..3f719142a
--- /dev/null
+++ b/ui/src/app/shared/memo.ts
@@ -0,0 +1,15 @@
+export function memoize(func) {
+ const cache = {};
+ return function (...args: any[]) {
+ const key = JSON.stringify(args);
+ if (cache[key]) {
+ return cache[key];
+ } else {
+ const val = func.apply(null, args);
+ cache[key] = val;
+ return val;
+ }
+ };
+}
+
+export default { memoize };
diff --git a/ui/src/app/shared/validation/regex.validator.spec.ts b/ui/src/app/shared/validation/regex.validator.spec.ts
new file mode 100644
index 000000000..ce8af17ee
--- /dev/null
+++ b/ui/src/app/shared/validation/regex.validator.spec.ts
@@ -0,0 +1,18 @@
+import { RegexValidator } from './regex.validator';
+
+describe('RegexValidator', () => {
+ describe('isValidRegex method', () => {
+ it('should return true if no error is thrown', () => {
+ expect(RegexValidator.isValidRegex('/abc/')).toBe(true);
+ expect(RegexValidator.isValidRegex('/*123/')).toBe(true);
+ });
+
+ it('should return false if an error is thrown trying to construct a regex', () => {
+ expect(RegexValidator.isValidRegex(')')).toBe(false);
+ });
+
+ it('should return false if the regex doesnt begin and end with slashes', () => {
+ expect(RegexValidator.isValidRegex('abc')).toBe(false);
+ });
+ });
+});
diff --git a/ui/src/app/shared/validation/regex.validator.ts b/ui/src/app/shared/validation/regex.validator.ts
new file mode 100644
index 000000000..be567a0f4
--- /dev/null
+++ b/ui/src/app/shared/validation/regex.validator.ts
@@ -0,0 +1,18 @@
+const regexChecker = new RegExp('^\/|\/$', 'g');
+
+export class RegexValidator {
+ static isValidRegex(pattern: string): boolean {
+ if (!pattern) {
+ return true;
+ }
+ let regex;
+ try {
+ regex = new RegExp(pattern);
+ } catch (err) {
+ return false;
+ }
+ return regexChecker.test(pattern);
+ }
+}
+
+export default RegexValidator;