diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy new file mode 100644 index 000000000..4da9b949f --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy @@ -0,0 +1,69 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.filesystemMetadataProviderSchema +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.localDynamicMetadataProviderSchema +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.dynamicHttpMetadataProviderSchema +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType + +/** + * Controller implementing REST resource responsible for exposing structure definition for metadata resolvers user + * interface in terms of JSON schema. + * + * @author Dmitriy Kopylenko + * @author Bill Smith (wsmith@unicon.net) + */ +@RestController +@RequestMapping('/api/ui/MetadataResolver') +class MetadataResolverUiDefinitionController { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + JsonSchemaResourceLocation jsonSchemaLocation + + @Autowired + ObjectMapper jacksonObjectMapper + + @GetMapping(value = "/{resolverType}") + ResponseEntity getUiDefinitionJsonSchema(@PathVariable String resolverType) { + switch (SchemaType.getSchemaType(resolverType)) { + case SchemaType.FILESYSTEM_METADATA_RESOLVER: + jsonSchemaLocation = filesystemMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) + break +/* case SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER: + jsonSchemaLocation = localDynamicMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) + break*/ +/* case SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER: + jsonSchemaLocation = dynamicHttpMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) + break*/ + default: + throw new UnsupportedOperationException("Json schema for an unsupported metadata resolver (" + resolverType + ") was requested") + } + try { + def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) + return ResponseEntity.ok(parsedJson) + } + catch (Exception e) { + e.printStackTrace() + return ResponseEntity.status(INTERNAL_SERVER_ERROR) + .body([jsonParseError : e.getMessage(), + sourceUiSchemaDefinitionFile: this.jsonSchemaLocation.url]) + } + } + + @GetMapping(value = "/types") + ResponseEntity getResolverTypes() { + return ResponseEntity.ok(SchemaType.getResolverTypes()) + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index 1bf2745c6..4c4ad86db 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -12,6 +12,9 @@ import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.*; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES; +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER; +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER; /** * @author Dmitriy Kopylenko @@ -30,6 +33,21 @@ public class JsonSchemaComponentsConfiguration { @Setter private String entityAttributesFiltersUiSchemaLocation = "classpath:entity-attributes-filters-ui-schema.json"; + //Configured via @ConfigurationProperties (using setter method) with 'shibui.filesystem-metadata-provider-ui-schema-location' property and + // default value set here if that property is not explicitly set in application.properties + @Setter + private String filesystemMetadataResolverUiSchemaLocation = "classpath:file-system-metadata-provider.schema.json"; + +/* TODO: Will be added as part of SHIBUI-703 + @Setter + private String localDynamicMetadataResolverUiSchemaLocation = "classpath:local-dynamic-metadata-provider.schema.json"; +*/ + +/* TODO: Will be added as part of SHIBUI-704 + @Setter + private String dynamicHttpMetadataResolverUiSchemaLocation = "classpath:dynamic-http-metadata-provider.schema.json"; +*/ + @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { return JsonSchemaResourceLocationRegistry.inMemory() @@ -44,7 +62,25 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(true) + .build()) + .register(FILESYSTEM_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(filesystemMetadataResolverUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) .build()); + /*.register(DYNAMIC_HTTP_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(dynamicHttpMetadataResolverUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) + .build()) + .register(LOCAL_DYNAMIC_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(localDynamicMetadataResolverUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) + .build());*/ } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 47458273d..4593d8bbf 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -34,6 +34,7 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringWriter; import java.net.URI; @@ -120,8 +121,10 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) throw //TODO: currently, the update call might explode, but the save works.. in which case, the UI never gets // n valid response. This operation is not atomic. Should we return an error here? - org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); - OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + if (persistedResolver.getDoInitialization()) { + org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + } return ResponseEntity.created(getResourceUriFor(persistedResolver)).body(persistedResolver); } @@ -148,8 +151,16 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); - org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); - OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + if (persistedResolver.getDoInitialization()) { + org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = null; + try { + openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); + } catch (FileNotFoundException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.toString(), "label.file-doesnt-exist")); + } + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + } return ResponseEntity.ok(persistedResolver); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java index 1370f0881..b96a74c14 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java @@ -19,6 +19,7 @@ public class FilesystemMetadataResolver extends MetadataResolver { public FilesystemMetadataResolver() { type = "FilesystemMetadataResolver"; + this.setDoInitialization(false); } private String metadataFile; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index 599503f75..8f2fbfd60 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -66,6 +66,8 @@ public class MetadataResolver extends AbstractAuditable { private Boolean satisfyAnyPredicates = false; + private Boolean doInitialization = true; + @OneToMany(cascade = CascadeType.ALL) @OrderColumn private List metadataFilters = new ArrayList<>(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java index b4fb6d578..8da204c3e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java @@ -51,11 +51,13 @@ public DateTime getLastRefresh() { @Override protected void initMetadataResolver() throws ComponentInitializationException { - super.initMetadataResolver(); + if (this.sourceResolver.getDoInitialization()) { + super.initMetadataResolver(); - delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), - this.sourceResolver.getResourceId(), - indexWriter); + delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), + this.sourceResolver.getResourceId(), + indexWriter); + } } /** diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java index e04dd72de..0083450b3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java @@ -2,6 +2,9 @@ import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES; +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER; +//import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER; /** * Utility methods for common JSON schema types lookups. @@ -27,7 +30,7 @@ public static JsonSchemaResourceLocation metadataSourcesSchema(JsonSchemaResourc * Searches entity attributes filters JSON schema resource location object in the given location registry. * * @param resourceLocationRegistry - * @returnentity attributes filters JSON schema resource location object + * @return entity attributes filters JSON schema resource location object * @throws IllegalStateException if schema is not found in the given registry */ public static JsonSchemaResourceLocation entityAttributesFiltersSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { @@ -35,4 +38,29 @@ public static JsonSchemaResourceLocation entityAttributesFiltersSchema(JsonSchem .lookup(ENTITY_ATTRIBUTES_FILTERS) .orElseThrow(() -> new IllegalStateException("JSON schema resource location for entity attributes filters is not registered.")); } + + /** + * Searches filesystem metadata resolver JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return filesystem metadata resolver JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + public static JsonSchemaResourceLocation filesystemMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry + .lookup(FILESYSTEM_METADATA_RESOLVER) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for filesystem metadata resolver is not registered.")); + } + +/* public static JsonSchemaResourceLocation localDynamicMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry + .lookup(LOCAL_DYNAMIC_METADATA_RESOLVER) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for local dynamic metadata resolver is not registered.")); + }*/ + +/* public static JsonSchemaResourceLocation dynamicHttpMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry + .lookup(DYNAMIC_HTTP_METADATA_RESOLVER) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for dynamic http metadata resolver is not registered.")); + }*/ } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java index f08ac6069..05280d45e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java @@ -9,7 +9,10 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Encapsulates arbitrary JSON schema location. @@ -91,6 +94,41 @@ public static JsonSchemaResourceLocation newSchemaLocation(String jsonSchemaLoca } public enum SchemaType { - METADATA_SOURCES, ENTITY_ATTRIBUTES_FILTERS + // common types + METADATA_SOURCES("MetadataSources"), + + // filter types + ENTITY_ATTRIBUTES_FILTERS("EntityAttributesFilters"), + + // resolver types + FILE_BACKED_HTTP_METADATA_RESOLVER("FileBackedHttpMetadataResolver"), + FILESYSTEM_METADATA_RESOLVER("FilesystemMetadataResolver"); +// LOCAL_DYNAMIC_METADATA_RESOLVER, +// DYNAMIC_HTTP_METADATA_RESOLVER; + + String jsonType; + + SchemaType(String jsonType) { + this.jsonType = jsonType; + } + + String getJsonType() { + return jsonType; + } + + public static List getResolverTypes() { + return Stream.of(SchemaType.values()).map(SchemaType::getJsonType).filter(it -> it.endsWith("Resolver")).collect(Collectors.toList()); + } + + public static SchemaType getSchemaType(String jsonType) { + List schemaTypes = Stream.of(SchemaType.values()).filter(it -> it.getJsonType().equals(jsonType)).collect(Collectors.toList()); + if (schemaTypes.size() > 1) { + throw new RuntimeException("Found multiple schema types for jsonType (" + jsonType + ")!"); + } else if (schemaTypes.size() == 0) { + throw new RuntimeException("Found no schema types for jsonType (" + jsonType + ")! Verify that the code supports all schema types."); + } else { + return schemaTypes.get(0); + } + } } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index 1ddcbb9a4..638a6307f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -25,6 +25,7 @@ import javax.validation.ConstraintViolationException; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; @@ -61,9 +62,10 @@ private OpenSamlFileBackedHTTPMetadataResolver convertToOpenSamlRepresentation(F private OpenSamlFilesystemMetadataResolver convertToOpenSamlRepresentation(FilesystemMetadataResolver resolver) throws IOException, ResolverException, ComponentInitializationException { IndexWriter indexWriter = indexWriterService.getIndexWriter(resolver.getResourceId()); - URL url = Thread.currentThread().getContextClassLoader().getResource(placeholderResolverService() - .resolveValueFromPossibleTokenPlaceholder(resolver.getMetadataFile())); - File metadataFile = new File(url.getPath()); + File metadataFile = new File(resolver.getMetadataFile()); + if (resolver.getDoInitialization() && !metadataFile.exists()) { + throw new FileNotFoundException("No file was found on the fileysystem for provided filename: " + resolver.getMetadataFile()); + } OpenSamlFilesystemMetadataResolver openSamlResolver = new OpenSamlFilesystemMetadataResolver(openSamlObjects.getParserPool(), indexWriter, diff --git a/backend/src/main/resources/file-system-metadata-provider.schema.json b/backend/src/main/resources/file-system-metadata-provider.schema.json new file mode 100644 index 000000000..8f28d3d83 --- /dev/null +++ b/backend/src/main/resources/file-system-metadata-provider.schema.json @@ -0,0 +1,144 @@ +{ + "type": "object", + "required": [ + "name", + "@type", + "xmlId", + "metadataFile" + ], + "properties": { + "name": { + "title": "label.metadata-provider-name", + "description": "tooltip.metadata-provider-name", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "placeholder": "label.select-metadata-type", + "type": "string", + "readOnly": true, + "widget": { + "id": "select", + "disabled": true + }, + "oneOf": [ + { + "enum": [ + "FileSystemMetadataResolver" + ], + "description": "value.file-system-metadata-provider" + } + ] + }, + "xmlId": { + "title": "label.xml-id", + "description": "tooltip.xml-id", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataFile": { + "title": "label.metadata-file", + "description": "tooltip.metadata-file", + "type": "string", + "default": "", + "minLength": 1 + }, + "enabled": { + "title": "label.enable-provider-upon-saving", + "description": "tooltip.enable-provider-upon-saving", + "type": "boolean", + "default": false + }, + "doInitialization": { + "title": "label.do-resolver-initialization", + "description": "tooltip.do-resolver-initialization", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": false + }, + "reloadableMetadataResolverAttributes": { + "type": "object", + "properties": { + "minRefreshDelay": { + "title": "label.min-refresh-delay", + "description": "tooltip.min-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "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)?)?$" + }, + "maxRefreshDelay": { + "title": "label.max-refresh-delay", + "description": "tooltip.max-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "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)?)?$" + }, + "refreshDelayFactor": { + "title": "label.refresh-delay-factor", + "description": "tooltip.refresh-delay-factor", + "type": "number", + "widget": { + "id": "number", + "step": 0.01 + }, + "placeholder": "label.real-number", + "minimum": 0, + "maximum": 1, + "default": null + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 83b644910..87aaf7299 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -70,6 +70,9 @@ value.encryption=Encryption value.both=Both value.file-backed-http-metadata-provider=FileBackedHttpMetadataProvider +value.file-system-metadata-provider=FileSystemMetadataProvider +value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider +value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor @@ -293,6 +296,8 @@ label.metadata-provider-status=Metadata Provider Status label.enable-provider-upon-saving=Enable Metadata Provider upon saving? label.certificate-type=Type +label.metadata-file=Metadata File + label.enable-filter=Enable Filter? label.required-valid-until=Required Valid Until Filter label.max-validity-interval=Max Validity Interval @@ -332,6 +337,9 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN +label.do-resolver-initialization=Initialize +label.file-doesnt-exist=The file specified in the resolver does not exist on the file system. Therefore, the resolver cannot be initialized. + message.must-be-unique=Must be unique. message.name-must-be-unique=Name must be unique. message.uri-valid-format=URI must be valid format. @@ -407,7 +415,7 @@ tooltip.authentication-methods-to-use=Authentication Methods to Use tooltip.ignore-auth-method=Ignore any SP-Requested Authentication Method tooltip.omit-not-before-condition=Omit Not Before Condition tooltip.responder-id=ResponderId -tooltip.instruction=Information icon - press spacebar to read additional information for this form field +tooltip.instruction=Information icon tooltip.attribute-release-table=Attribute release table - select the attributes you want to release (default unchecked) tooltip.metadata-filter-name=Metadata Filter Name tooltip.metadata-filter-type=Metadata Filter Type @@ -433,7 +441,7 @@ tooltip.backing-file=Specifies where the backing file is located. If the remote tooltip.backup-file-init-refresh-delay=Delay duration after which to schedule next HTTP refresh when initialized from the backing file. tooltip.require-valid-metadata=Whether candidate metadata found by the resolver must be valid in order to be returned (where validity is implementation specific, but in SAML cases generally depends on a validUntil attribute.) If this flag is true, then invalid candidate metadata will not be returned. tooltip.fail-fast-init=Whether to fail initialization of the underlying MetadataResolverService (and possibly the IdP as a whole) if the initialization of a metadata provider fails. When false, the IdP may start, and will continue to attempt to reload valid metadata if configured to do so, but operations that require valid metadata will fail until it does. -tooltip.use-default-predicate-reg=Whether to fail initialization of the underlying MetadataResolverService (and possibly the IdP as a whole) if the initialization of a metadata provider fails. When false, the IdP may start, and will continue to attempt to reload valid metadata if configured to do so, but operations that require valid metadata will fail until it does. +tooltip.use-default-predicate-reg=Flag which determines whether the default CriterionPredicateRegistry will be used if a custom one is not supplied explicitly. tooltip.satisfy-any-predicates=Flag which determines whether predicates used in filtering are connected by a logical 'OR' (true) or by logical 'AND' (false). tooltip.enable-provider-upon-saving=Enable Metadata Provider upon saving? @@ -453,3 +461,5 @@ tooltip.expiration-warning-threshold=For each attempted metadata refresh (whethe tooltip.filter-name=Filter Name tooltip.enable-filter=Enable Filter? tooltip.enable-service=Enable Service? + +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. diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index bf1d807bf..e156a803d 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -70,6 +70,9 @@ value.encryption=Encryption value.both=Both value.file-backed-http-metadata-provider=FileBackedHttpMetadataProvider +value.file-system-metadata-provider=FileSystemMetadataProvider +value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider +value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor @@ -294,6 +297,8 @@ label.metadata-provider-status=Metadata Provider Status label.enable-provider-upon-saving=Enable Metadata Provider upon saving? label.certificate-type=Type +label.metadata-file=Metadata File + label.enable-filter=Enable Filter? label.required-valid-until=Required Valid Until Filter label.max-validity-interval=Max Validity Interval @@ -333,6 +338,9 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN +label.do-resolver-initialization=Initialize +label.file-doesnt-exist=The file specified in the resolver does not exist on the file system. Therefore, the resolver cannot be initialized. + message.must-be-unique=Must be unique. message.name-must-be-unique=Name must be unique. message.uri-valid-format=URI must be valid format. @@ -429,6 +437,7 @@ tooltip.metadata-provider-name=Metadata Provider Name tooltip.metadata-provider-type=Metadata Provider Type tooltip.xml-id=Identifier for logging, identification for command line reload, etc. tooltip.metadata-url=Identifier for logging, identification for command line reload, etc. +tooltip.metadata-file=The absolute path to the local metadata file to be loaded. tooltip.init-from-backup=Flag indicating whether initialization should first attempt to load metadata from the backup file. If true, foreground initialization will be performed by loading the backing file, and then a refresh from the remote HTTP server will be scheduled to execute in a background thread, after a configured delay. This can improve IdP startup times when the remote HTTP file is large in size. tooltip.backing-file=Specifies where the backing file is located. If the remote server is unavailable at startup, the backing file is loaded instead. tooltip.backup-file-init-refresh-delay=Delay duration after which to schedule next HTTP refresh when initialized from the backing file. @@ -454,3 +463,5 @@ tooltip.expiration-warning-threshold=For each attempted metadata refresh (whethe tooltip.filter-name=Filter Name tooltip.enable-filter=Enable Filter? tooltip.enable-service=Enable Service? + +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. diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy index c881d81c7..5c4efc0b6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -13,6 +13,7 @@ import spock.lang.Specification import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.* import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES /** @@ -50,12 +51,19 @@ class BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests extends Speci .jacksonMapper(jacksonMapper) .detectMalformedJson(false) .build()) - .register(ENTITY_ATTRIBUTES_FILTERS, JsonSchemaLocationBuilder.with() + //TODO Maybe we need a separate test config here so we don't have to define all of the locations? + .register(ENTITY_ATTRIBUTES_FILTERS, JsonSchemaLocationBuilder.with() .jsonSchemaLocation('classpath:entity-attributes-filters-ui-schema.json') .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(false) .build()) + .register(FILESYSTEM_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation('classpath:file-system-metadata-provider.schema.json') + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(false) + .build()) } } } \ No newline at end of file diff --git a/ui/src/app/metadata/domain/model/providers/file-system-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/file-system-metadata-provider.ts new file mode 100644 index 000000000..e884151c1 --- /dev/null +++ b/ui/src/app/metadata/domain/model/providers/file-system-metadata-provider.ts @@ -0,0 +1,7 @@ +import { BaseMetadataProvider } from './base-metadata-provider'; + +export interface FileSystemMetadataProvider extends BaseMetadataProvider { + id: string; + metadataFile: string; + reloadableMetadataResolverAttributes: any; +} diff --git a/ui/src/app/metadata/manager/component/provider-item.component.html b/ui/src/app/metadata/manager/component/provider-item.component.html index f55293a4e..a4270ce3d 100644 --- a/ui/src/app/metadata/manager/component/provider-item.component.html +++ b/ui/src/app/metadata/manager/component/provider-item.component.html @@ -37,7 +37,7 @@
- diff --git a/ui/src/app/metadata/manager/component/provider-item.component.ts b/ui/src/app/metadata/manager/component/provider-item.component.ts index ad8fa572b..514c5b9fe 100644 --- a/ui/src/app/metadata/manager/component/provider-item.component.ts +++ b/ui/src/app/metadata/manager/component/provider-item.component.ts @@ -2,6 +2,7 @@ import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter } from import { MetadataProvider } from '../../domain/model'; import { EntityItemComponent } from './entity-item.component'; +import { FilterableProviders } from '../../provider/model'; @Component({ selector: 'provider-item', @@ -20,4 +21,8 @@ export class ProviderItemComponent extends EntityItemComponent { @Output() changeOrderUp: EventEmitter = new EventEmitter(); @Output() changeOrderDown: EventEmitter = new EventEmitter(); + + hasFilters(provider: MetadataProvider): boolean { + return FilterableProviders.indexOf(provider['@type']) > -1; + } } diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts index ec83223fd..2a32d925d 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts @@ -49,7 +49,7 @@ export class ProviderEditStepComponent implements OnDestroy { this.changes$ = this.store.select(fromProvider.getEntityChanges); this.provider$ = this.store.select(fromProvider.getSelectedProvider); this.step$ = this.store.select(fromWizard.getCurrent); - this.schema$ = this.store.select(fromWizard.getParsedSchema); + this.schema$ = this.store.select(fromWizard.getSchema); this.step$.subscribe(s => { if (s && s.locked) { diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.html b/ui/src/app/metadata/provider/container/provider-edit.component.html index ffba0f5ff..0011e0ceb 100644 --- a/ui/src/app/metadata/provider/container/provider-edit.component.html +++ b/ui/src/app/metadata/provider/container/provider-edit.component.html @@ -14,6 +14,7 @@ [format]="formats.DROPDOWN" [status]="status$ | async"> ; status$: Observable; isSaving$: Observable; + canFilter$: Observable; latest: MetadataProvider; provider: MetadataProvider; @@ -70,6 +72,8 @@ export class ProviderEditComponent implements OnDestroy, CanComponentDeactivate this.provider$.subscribe(p => this.provider = p); this.store.select(fromProvider.getEntityChanges).subscribe(changes => this.latest = changes); + + this.canFilter$ = this.definition$.pipe(map(def => FilterableProviders.indexOf(def.type) > -1)); } go(id: string): void { diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts index 73ce52ea5..aa80ae3c9 100644 --- a/ui/src/app/metadata/provider/effect/collection.effect.ts +++ b/ui/src/app/metadata/provider/effect/collection.effect.ts @@ -37,6 +37,11 @@ import { ClearProvider, ResetChanges } from '../action/entity.action'; import { ShowContentionAction } from '../../../contention/action/contention.action'; import { ContentionService } from '../../../contention/service/contention.service'; import { MetadataProvider } from '../../domain/model'; +import { AddNotification } from '../../../notification/action/notification.action'; +import { Notification, NotificationType } from '../../../notification/model/notification'; +import { WizardActionTypes, SetDisabled } from '../../../wizard/action/wizard.action'; +import { I18nService } from '../../../i18n/service/i18n.service'; +import * as fromI18n from '../../../i18n/reducer'; /* istanbul ignore next */ @@ -96,11 +101,31 @@ export class CollectionEffects { .save(provider) .pipe( map(p => new AddProviderSuccess(p)), - catchError((e) => of(new AddProviderFail(e))) + catchError((e) => of(new AddProviderFail(e.error))) ) ) ); + @Effect() + createProviderFailDispatchNotification$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.ADD_PROVIDER_FAIL), + map(action => action.payload), + withLatestFrom(this.store.select(fromI18n.getMessages)), + map(([error, messages]) => new AddNotification( + new Notification( + NotificationType.Danger, + `${error.errorCode}: ${ this.i18nService.translate(error.errorMessage, null, messages) }`, + 8000 + ) + )) + ); + @Effect() + createProviderFailEnableForm$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.ADD_PROVIDER_FAIL), + map(action => action.payload), + map(error => new SetDisabled(false)) + ); + @Effect({ dispatch: false }) createProviderSuccessRedirect$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS), @@ -220,6 +245,7 @@ export class CollectionEffects { private router: Router, private store: Store, private providerService: MetadataProviderService, - private contentionService: ContentionService + private contentionService: ContentionService, + private i18nService: I18nService ) { } } diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts index 8dbe80204..f6bd3796f 100644 --- a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts @@ -1,7 +1,7 @@ import { Wizard } from '../../../wizard/model'; import { FileBackedHttpMetadataProvider } from '../../domain/model/providers/file-backed-http-metadata-provider'; import { BaseMetadataProviderEditor } from './base.provider.form'; -import UriValidator from '../../../shared/validation/uri.validator'; +import { UriValidator } from '../../../shared/validation/uri.validator'; export const FileBackedHttpMetadataProviderWizard: Wizard = { ...BaseMetadataProviderEditor, @@ -20,13 +20,12 @@ export const FileBackedHttpMetadataProviderWizard: Wizard { return !UriValidator.isUri(value) ? { - code : 'INVALID_URI', + code: 'INVALID_URI', path: `#${property.path}`, message: 'message.uri-valid-format', params: [value] } : null; }; - return validators; }, steps: [ diff --git a/ui/src/app/metadata/provider/model/file-system.provider.form.ts b/ui/src/app/metadata/provider/model/file-system.provider.form.ts new file mode 100644 index 000000000..9c5e2536b --- /dev/null +++ b/ui/src/app/metadata/provider/model/file-system.provider.form.ts @@ -0,0 +1,154 @@ +import { Wizard } from '../../../wizard/model'; +import { FileSystemMetadataProvider } from '../../domain/model/providers/file-system-metadata-provider'; +import { BaseMetadataProviderEditor } from './base.provider.form'; + +export const FileSystemMetadataProviderWizard: Wizard = { + ...BaseMetadataProviderEditor, + label: 'FilesystemMetadataProvider', + type: 'FilesystemMetadataResolver', + getValidators(namesList: string[] = [], xmlIdList: string[] = []): any { + const validators = BaseMetadataProviderEditor.getValidators(namesList); + validators['/xmlId'] = (value, property, form) => { + const err = xmlIdList.indexOf(value) > -1 ? { + code: 'INVALID_ID', + path: `#${property.path}`, + message: 'message.id-unique', + params: [value] + } : null; + return err; + }; + return validators; + }, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', + fields: [ + 'xmlId', + 'metadataFile', + 'doInitialization' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'metadataFile', + 'doInitialization' + ] + } + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 3, + initialValues: [], + schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', + fields: [ + 'reloadableMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + } + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 4, + initialValues: [], + schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', + fields: [ + 'enabled' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled' + ] + } + ] + } + ] +}; + + +export const FileSystemMetadataProviderEditor: Wizard = { + ...FileSystemMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', + fields: [ + 'name', + 'xmlId', + '@type', + 'metadataFile', + 'enabled', + 'doInitialization' + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['FilesystemMetadataResolver'], description: 'value.file-system-metadata-provider'}] + } + }, + fieldsets: [ + { + type: 'section', + class: ['mb-3'], + fields: [ + 'name', + '@type', + 'enabled' + ] + }, + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'metadataFile', + 'doInitialization' + ] + } + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 2, + initialValues: [], + schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', + fields: [ + 'reloadableMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + } + ] + } + ] +}; diff --git a/ui/src/app/metadata/provider/model/index.ts b/ui/src/app/metadata/provider/model/index.ts index cfb64a08d..278652334 100644 --- a/ui/src/app/metadata/provider/model/index.ts +++ b/ui/src/app/metadata/provider/model/index.ts @@ -1,12 +1,19 @@ import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form'; import { FileBackedHttpMetadataProviderEditor } from './file-backed-http.provider.form'; +import { FileSystemMetadataProviderWizard, FileSystemMetadataProviderEditor } from './file-system.provider.form'; export const MetadataProviderWizardTypes = [ - FileBackedHttpMetadataProviderWizard + FileBackedHttpMetadataProviderWizard, + FileSystemMetadataProviderWizard ]; export const MetadataProviderEditorTypes = [ - FileBackedHttpMetadataProviderEditor + FileBackedHttpMetadataProviderEditor, + FileSystemMetadataProviderEditor +]; + +export const FilterableProviders = [ + FileBackedHttpMetadataProviderEditor.type ]; export * from './file-backed-http.provider.form'; diff --git a/ui/src/app/metadata/provider/model/provider.form.ts b/ui/src/app/metadata/provider/model/provider.form.ts index 71ba6174b..26fbdd966 100644 --- a/ui/src/app/metadata/provider/model/provider.form.ts +++ b/ui/src/app/metadata/provider/model/provider.form.ts @@ -1,6 +1,5 @@ import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; -import { Metadata } from '../../domain/domain.type'; import { BaseMetadataProviderEditor } from './base.provider.form'; export const MetadataProviderWizard: Wizard = { @@ -29,5 +28,5 @@ export const MetadataProviderWizard: Wizard = { } ] } - ] as WizardStep[] + ] as WizardStep[], }; diff --git a/ui/src/app/schema-form/widget/select/select.component.html b/ui/src/app/schema-form/widget/select/select.component.html index 7862ec0ca..806945bed 100644 --- a/ui/src/app/schema-form/widget/select/select.component.html +++ b/ui/src/app/schema-form/widget/select/select.component.html @@ -25,7 +25,16 @@ {{ schema.placeholder }} {{ schema.title }} - + + + + + +