diff --git a/backend/build.gradle b/backend/build.gradle index f03f9f9ed..3b6f64ddc 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -139,6 +139,9 @@ dependencies { //JSON schema validator compile 'org.sharegov:mjson:1.4.1' + + // CSV file support + compile 'com.opencsv:opencsv:4.4' } def generatedSrcDir = new File(buildDir, 'generated/src/main/java') diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy new file mode 100644 index 000000000..d51cc4d01 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy @@ -0,0 +1,48 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import com.opencsv.CSVReader +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import groovy.util.logging.Slf4j +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +import javax.transaction.Transactional + +@Component +@Slf4j +class UserBootstrap { + private final ShibUIConfiguration shibUIConfiguration + private final UserRepository userRepository + private final RoleRepository roleRepository + + UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository) { + this.shibUIConfiguration = shibUIConfiguration + this.userRepository = userRepository + this.roleRepository = roleRepository + } + + @Transactional + @EventListener + void bootstrapUsersAndRoles(ApplicationStartedEvent e) { + if (shibUIConfiguration.userBootstrapResource) { + log.info("configuring users from ${shibUIConfiguration.userBootstrapResource.URI}") + new CSVReader(new InputStreamReader(shibUIConfiguration.userBootstrapResource.inputStream)).each { it -> + def (username, password, firstName, lastName, roleName) = it + def role = roleRepository.findByName(roleName).orElse(roleRepository.save(new Role(name: roleName))) + def user = userRepository.findByUsername(username).orElse(new User(username: username)).with { + it.password = password + it.firstName = firstName + it.lastName = lastName + it.roles.add(role) + it + } + userRepository.save(user) + } + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java index 953ed2119..3c85674e9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java @@ -4,8 +4,10 @@ import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; import java.util.List; +import java.util.Optional; @Configuration @ConfigurationProperties(prefix = "shibui") @@ -17,4 +19,19 @@ public class ShibUIConfiguration { * sources from using attributes that they might not have the rights to use. */ private List protectedAttributeNamespaces; + + /** + * A Resource containing a CSV of users to bootstrap into the system. Currently, this must be in format + * + * + * username,password,firstName,lastName,role + * + * + * Note that the password must be encrypted in the file. Ensure that you prepend the encoder to the value, e.g. + * + * + * {bcrypt}$2a$10$ssM2LpFqceRQ/ta0JehGcu0BawFQDbxjQGSyVmKS6qa09hHLigtAO + * + */ + private Resource userBootstrapResource; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java index ce93b7615..272e178dc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java @@ -45,28 +45,22 @@ public List getAll() { } @Transactional(readOnly = true) - @GetMapping("/user/{username}") + @GetMapping("/users/{username}") public ResponseEntity getOne(@PathVariable String username) { return ResponseEntity.ok(findUserOrThrowHttp404(username)); } @Transactional - @DeleteMapping("/user/{username}") + @DeleteMapping("/users/{username}") public ResponseEntity deleteOne(@PathVariable String username) { User user = findUserOrThrowHttp404(username); userRepository.delete(user); return ResponseEntity.noContent().build(); } - - private User findUserOrThrowHttp404(String username) { - return userRepository.findByUsername(username) - .orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username))); - } - @Transactional - @PostMapping("/user") - ResponseEntity saveUser(@RequestParam User user) { + @PostMapping("/users") + ResponseEntity saveOne(@RequestParam User user) { Optional persistedUser = userRepository.findByUsername(user.getUsername()); if (persistedUser.isPresent()) { return ResponseEntity @@ -80,8 +74,8 @@ ResponseEntity saveUser(@RequestParam User user) { } @Transactional - @PutMapping("/user/{username}") - ResponseEntity updateUser(@PathVariable(value = "username") String username, @RequestParam User user) { + @PutMapping("/users/{username}") + ResponseEntity updateOne(@PathVariable(value = "username") String username, @RequestParam User user) { Optional userSearchResult = userRepository.findByUsername(username); if (!userSearchResult.isPresent()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) @@ -97,4 +91,9 @@ ResponseEntity updateUser(@PathVariable(value = "username") String username, User savedUser = userRepository.save(persistedUser); return ResponseEntity.ok(savedUser); } + + private User findUserOrThrowHttp404(String username) { + return userRepository.findByUsername(username) + .orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username))); + } } diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json index 887508b7f..1969c9ab4 100644 --- a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -25,35 +25,6 @@ "@type", "content" ], - "anyOf": [ - { - "properties": { - "@type": { - "enum": [ - "Regex" - ] - } - }, - "required": [ - "@type", - "content", - "match" - ] - }, - { - "properties": { - "@type": { - "enum": [ - "MetadataQueryProtocol" - ] - } - }, - "required": [ - "@type", - "content" - ] - } - ], "properties": { "@type": { "title": "label.md-request-type", diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index 225f550d7..009bc1a08 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -304,7 +304,7 @@ label.metadata-provider-type=Metadata Provider Type label.metadata-provider-name=Metadata Provider Name label.select-metadata-type=Select a metadata provider type label.metadata-provider-status=Metadata Provider Status -label.enable-provider-upon-saving=If checkbox is clicked, the metadata provider is enabled for integration with the IdP +label.enable-provider-upon-saving=Enable Metadata Provider? label.certificate-type=Type label.metadata-file=Metadata File @@ -391,6 +391,10 @@ message.org-displayName-required=Organization Name is required. message.org-url-required=Organization Name is required. message.org-incomplete=These three fields must all be entered if any single field has a value. +message.type-required=Missing required property: Type +message.match-required=Missing required property: Match +message.value-required=Missing required property: Value + message.conflict=Conflict message.data-version-contention=Data Version Contention message.contention-new-version=A newer version of this metadata source has been saved. Below are a list of changes. You can use your changes or their changes. @@ -437,7 +441,7 @@ tooltip.assertion-consumer-service-location-binding=Assertion Consumer Service L tooltip.mark-as-default=Mark as Default tooltip.protocol-support-enumeration=Protocol Support Enumeration tooltip.nameid-format=Content is name identifier format which is added to all the applicable roles of the entities which match any of the following or {{}}elements. -tooltip.enable-this-service-upon-saving=Enable this service upon saving +tooltip.enable-this-service-upon-saving=If checkbox is clicked, the metadata provider is enabled for integration with the IdP tooltip.authentication-requests-signed=Authentication Requests Signed tooltip.want-assertions-signed=Want Assertions Signed tooltip.certificate-name=Certificate Name @@ -523,7 +527,7 @@ tooltip.source-directory=Convenience mechanism for wiring a FilesystemLoadSaveMa tooltip.remove-idle-entity-data=Flag indicating whether idle metadata should be removed. tooltip.do-resolver-initialization=Initialize this resolver? In the case of Filesystem resolvers, this will cause the system to read the file and index the resolver. -tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Template, 3) Regex. +tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Regex. tooltip.md-request-value=Content of the element. tooltip.transform-ref=A reference to a transform function for the entityID. If used, the child element must be empty. tooltip.encoding-style=Determines whether and how the entityID value will be URL encoded prior to replacement. Allowed values are: 1) "none" - no encoding is performed, 2) "form" - encoded using URL form parameter encoding (for query parameters), 3) "path" - encoded using URL path encoding, or 4) "fragment" - encoded using URL fragment encoding. The precise definition of these terms is defined in the documentation for the methods of the Guava library\u0027s UrlEscapers class. diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy index 14d38e6b8..7c97f7475 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ActiveProfiles import spock.lang.Specification @@ -48,6 +49,7 @@ class UsersControllerIntegrationTests extends Specification { result.body.errorMessage == 'User with username [bogus] not found' } + @DirtiesContext def 'DELETE ONE existing user'() { when: 'GET request is made for one existing user' def result = this.restTemplate.getForEntity("$RESOURCE_URI/admin", Map) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy new file mode 100644 index 000000000..74f6df359 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.core.io.ClassPathResource +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, ShibUIConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan(["edu.internet2.tier.shibboleth.admin.ui", "edu.internet2.tier.shibboleth.admin.ui.security.model"]) +@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) +class UserBootstrapTests extends Specification { + @Autowired + ShibUIConfiguration shibUIConfiguration + + @Autowired + UserRepository userRepository + + @Autowired + RoleRepository roleRepository + + def "simple test"() { + setup: + shibUIConfiguration.userBootstrapResource = new ClassPathResource('/conf/1044.csv') + def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + + when: + userBootstrap.bootstrapUsersAndRoles(null) + + then: + noExceptionThrown() + assert userRepository.findAll().size() == 2 + assert roleRepository.findAll().size() == 2 + } +} diff --git a/backend/src/test/resources/conf/1044.csv b/backend/src/test/resources/conf/1044.csv new file mode 100644 index 000000000..666681a0b --- /dev/null +++ b/backend/src/test/resources/conf/1044.csv @@ -0,0 +1,2 @@ +"user1","password1","firstName1","lastName1","ROLE_ADMIN" +"user2","password2","firstName2","lastName2","ROLE_USER" \ No newline at end of file diff --git a/ui/src/app/metadata/domain/service/resolver.service.ts b/ui/src/app/metadata/domain/service/resolver.service.ts index 4012f9f23..895e086f0 100644 --- a/ui/src/app/metadata/domain/service/resolver.service.ts +++ b/ui/src/app/metadata/domain/service/resolver.service.ts @@ -46,7 +46,7 @@ export class ResolverService { headers: new HttpHeaders().set('Content-Type', 'application/xml'), params: new HttpParams().set('spName', name) }).pipe(catchError(error => { - return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error}` }); + return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error.errorMessage}` }); })); } @@ -56,7 +56,7 @@ export class ResolverService { headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'), params: new HttpParams().set('spName', name) }).pipe(catchError(error => { - return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error}` }); + return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error.errorMessage}` }); })); } diff --git a/ui/src/app/metadata/filter/action/filter.action.ts b/ui/src/app/metadata/filter/action/filter.action.ts index 1034d9e09..a745d78c4 100644 --- a/ui/src/app/metadata/filter/action/filter.action.ts +++ b/ui/src/app/metadata/filter/action/filter.action.ts @@ -7,6 +7,7 @@ export enum FilterActionTypes { SELECT_FILTER_TYPE = '[Filter] Select Filter Type', UPDATE_FILTER = '[Filter] Update Filter', CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter', + CLEAR_FILTER = '[Filter] Clear Filter', LOAD_ENTITY_PREVIEW = '[Filter] Load Preview data', LOAD_ENTITY_PREVIEW_SUCCESS = '[Filter] Load Preview data success', LOAD_ENTITY_PREVIEW_ERROR = '[Filter] Load Preview data error' @@ -38,6 +39,10 @@ export class CancelCreateFilter implements Action { readonly type = FilterActionTypes.CANCEL_CREATE_FILTER; } +export class ClearFilter implements Action { + readonly type = FilterActionTypes.CLEAR_FILTER; +} + export class UpdateFilterChanges implements Action { readonly type = FilterActionTypes.UPDATE_FILTER; @@ -57,4 +62,5 @@ export type FilterActionsUnion = | CancelCreateFilter | LoadEntityPreview | LoadEntityPreviewSuccess - | LoadEntityPreviewError; + | LoadEntityPreviewError + | ClearFilter; diff --git a/ui/src/app/metadata/filter/container/new-filter.component.ts b/ui/src/app/metadata/filter/container/new-filter.component.ts index 7fa2a5c7f..c9b686777 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.ts +++ b/ui/src/app/metadata/filter/container/new-filter.component.ts @@ -58,18 +58,21 @@ export class NewFilterComponent implements OnDestroy, OnInit { this.model = {}; this.definition$ = this.store.select(fromFilter.getFilterType).pipe( + takeUntil(this.ngUnsubscribe), filter(t => !!t), map(t => MetadataFilterTypes[t]) ); this.schema$ = this.definition$.pipe( + takeUntil(this.ngUnsubscribe), filter(d => !!d), switchMap(d => { - return this.schemaService.get(d.schema); + return this.schemaService.get(d.schema).pipe(takeUntil(this.ngUnsubscribe)); }), shareReplay() ); this.validators$ = this.definition$.pipe( + takeUntil(this.ngUnsubscribe), withLatestFrom(this.store.select(fromFilter.getFilterNames)), map(([definition, names]) => definition.getValidators(names)) ); @@ -107,7 +110,6 @@ export class NewFilterComponent implements OnDestroy, OnInit { } save(): void { - console.log(this.filter); this.store.dispatch(new AddFilterRequest(this.filter)); } diff --git a/ui/src/app/metadata/filter/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts index 65d5f8d2c..392cc0115 100644 --- a/ui/src/app/metadata/filter/effect/collection.effect.ts +++ b/ui/src/app/metadata/filter/effect/collection.effect.ts @@ -39,7 +39,7 @@ import { removeNulls, array_move } from '../../../shared/util'; import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; import { MetadataFilterService } from '../../domain/service/filter.service'; import { SelectProviderRequest } from '../../provider/action/collection.action'; -import { UpdateFilterChanges } from '../action/filter.action'; +import { UpdateFilterChanges, ClearFilter } from '../action/filter.action'; /* istanbul ignore next */ @Injectable() @@ -112,6 +112,12 @@ export class FilterCollectionEffects { map(([filter, provider]) => new SelectProviderRequest(provider)) ); + @Effect() + addFilterSuccessResetState$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), + map(() => new ClearFilter()) + ); + @Effect() updateFilter$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST), diff --git a/ui/src/app/metadata/filter/model/nameid.filter.ts b/ui/src/app/metadata/filter/model/nameid.filter.ts index 3787af80b..f3cf67960 100644 --- a/ui/src/app/metadata/filter/model/nameid.filter.ts +++ b/ui/src/app/metadata/filter/model/nameid.filter.ts @@ -3,7 +3,7 @@ import { MetadataFilter } from '../../domain/model'; import { NameIDFormatFilterEntity } from '../../domain/entity/filter/nameid-format-filter'; export const NameIDFilter: FormDefinition = { - label: 'NameIDFilter', + label: 'NameIDFormat', type: 'NameIDFormat', schema: '/api/ui/NameIdFormatFilter', getEntity(filter: MetadataFilter): NameIDFormatFilterEntity { diff --git a/ui/src/app/metadata/filter/reducer/filter.reducer.ts b/ui/src/app/metadata/filter/reducer/filter.reducer.ts index 9b47e8df0..68aaf40ed 100644 --- a/ui/src/app/metadata/filter/reducer/filter.reducer.ts +++ b/ui/src/app/metadata/filter/reducer/filter.reducer.ts @@ -47,6 +47,7 @@ export function reducer(state = initialState, action: FilterActionsUnion): Filte } }; } + case FilterActionTypes.CLEAR_FILTER: case FilterActionTypes.CANCEL_CREATE_FILTER: { return { ...initialState diff --git a/ui/src/app/metadata/provider/model/base.provider.form.spec.ts b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts index 7e1aeff61..99adaa6fa 100644 --- a/ui/src/app/metadata/provider/model/base.provider.form.spec.ts +++ b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts @@ -129,13 +129,14 @@ describe('BaseMetadataProviderForm', () => { describe('parent `/` validator', () => { const validators = BaseMetadataProviderEditor.getValidators(['foo', 'bar']); + const prop = { path: '/name', properties: { name: { type: 'string' } } }; it('should return a list of child errors', () => { - expect(validators['/']({name: 'foo'}, { path: '/name' }, {}).length).toBe(1); + expect(validators['/']({name: 'foo'}, prop, {}).length).toBe(1); }); it('should ignore properties that don\'t exist a list of child errors', () => { - expect(validators['/']({ foo: 'bar' }, { path: '/foo' }, {})).toBeUndefined(); + expect(validators['/']({ foo: 'bar' }, prop, {})).toBeUndefined(); }); }); }); diff --git a/ui/src/app/metadata/provider/model/base.provider.form.ts b/ui/src/app/metadata/provider/model/base.provider.form.ts index ecbcab666..e341a2fa5 100644 --- a/ui/src/app/metadata/provider/model/base.provider.form.ts +++ b/ui/src/app/metadata/provider/model/base.provider.form.ts @@ -8,12 +8,11 @@ export const BaseMetadataProviderEditor: Wizard = { const validators = { '/': (value, property, form_current) => { let errors; - // iterate all customer Object.keys(value).forEach((key) => { const item = value[key]; const validatorKey = `/${key}`; const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; - const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + const error = validator ? validator(item, property.properties[key], form_current) : null; if (error) { errors = errors || []; errors.push(error); diff --git a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts index 7f38d02e3..6ebf388d2 100644 --- a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts @@ -23,13 +23,54 @@ export const DynamicHttpMetadataProviderWizard: Wizard { - return !UriValidator.isUri(value) ? { - code: 'INVALID_URI', + + validators['/metadataRequestURLConstructionScheme'] = (value, property, form) => { + let errors; + let keys = Object.keys(property.schema.properties); + + keys.forEach((item) => { + const path = `/metadataRequestURLConstructionScheme/${item}`; + const error = validators[path](value[item], property.properties[item], form); + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }; + + validators['/metadataRequestURLConstructionScheme/content'] = (value, property, form) => { + const err = !value ? { + code: 'REQUIRED', path: `#${property.path}`, - message: 'message.uri-valid-format', + message: 'message.value-required', params: [value] } : null; + return err; + }; + + validators['/metadataRequestURLConstructionScheme/@type'] = (value, property, form) => { + const err = !value ? { + code: 'REQUIRED', + path: `#${property.path}`, + message: 'message.type-required', + params: [value] + } : null; + return err; + }; + + validators['/metadataRequestURLConstructionScheme/match'] = (value, property, form) => { + if (!property.parent || !property.parent.value) { + return null; + } + const isRegex = property.parent.value['@type'] === 'Regex'; + const err = isRegex && !value ? { + code: 'REQUIRED', + path: `#${property.path}`, + message: 'message.match-required', + params: [value] + } : null; + return err; }; return validators; @@ -43,7 +84,6 @@ export const DynamicHttpMetadataProviderWizard: Wizard + + + , + error + + diff --git a/ui/src/assets/schema/provider/dynamic-http.schema.json b/ui/src/assets/schema/provider/dynamic-http.schema.json index f9dec02b8..1969c9ab4 100644 --- a/ui/src/assets/schema/provider/dynamic-http.schema.json +++ b/ui/src/assets/schema/provider/dynamic-http.schema.json @@ -25,35 +25,6 @@ "@type", "content" ], - "anyOf": [ - { - "properties": { - "@type": { - "enum": [ - "Regex" - ] - } - }, - "required": [ - "@type", - "content", - "match" - ] - }, - { - "properties": { - "@type": { - "enum": [ - "MetadataQueryProtocol" - ] - } - }, - "required": [ - "@type", - "content" - ] - } - ], "properties": { "@type": { "title": "label.md-request-type", @@ -152,8 +123,8 @@ "step": 0.01 }, "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, + "minimum": 0.01, + "maximum": 0.99, "default": null }, "minCacheDuration": { @@ -222,6 +193,29 @@ "default": null, "pattern": "^(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)?)?$" }, + "removeIdleEntityData": { + "title": "label.remove-idle-entity-data", + "description": "tooltip.remove-idle-entity-data", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, "cleanupTaskInterval": { "title": "label.cleanup-task-interval", "description": "tooltip.cleanup-task-interval",