diff --git a/backend/build.gradle b/backend/build.gradle index 1ecb7fea8..f03f9f9ed 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -136,6 +136,9 @@ dependencies { //JSON schema generator testCompile 'com.kjetland:mbknor-jackson-jsonschema_2.12:1.0.29' testCompile 'javax.validation:validation-api:2.0.1.Final' + + //JSON schema validator + compile 'org.sharegov:mjson:1.4.1' } def generatedSrcDir = new File(buildDir, 'generated/src/main/java') diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityAttributesFiltersUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityAttributesFiltersUiDefinitionController.groovy new file mode 100644 index 000000000..ffd3ee3ab --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityAttributesFiltersUiDefinitionController.groovy @@ -0,0 +1,61 @@ +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 edu.internet2.tier.shibboleth.admin.ui.service.JsonSchemaBuilderService +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.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import javax.annotation.PostConstruct + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.entityAttributesFiltersSchema +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR + +/** + * Controller implementing REST resource responsible for exposing structure definition for metadata sources user + * interface in terms of JSON schema. + * + * @author Dmitriy Kopylenko + * @author Bill Smith (wsmith@unicon.net) + */ +@RestController +@RequestMapping('/api/ui/EntityAttributesFilters') +class EntityAttributesFiltersUiDefinitionController { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + JsonSchemaResourceLocation jsonSchemaLocation + + @Autowired + ObjectMapper jacksonObjectMapper + + @Autowired + JsonSchemaBuilderService jsonSchemaBuilderService + + @GetMapping + ResponseEntity getUiDefinitionJsonSchema() { + try { + def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) + jsonSchemaBuilderService.addReleaseAttributesToJson(parsedJson['properties']['attributeRelease']['widget']) + jsonSchemaBuilderService.addRelyingPartyOverridesToJson(parsedJson['properties']['relyingPartyOverrides']) + jsonSchemaBuilderService.addRelyingPartyOverridesCollectionDefinitionsToJson(parsedJson["definitions"]) + return ResponseEntity.ok(parsedJson) + } + catch (Exception e) { + e.printStackTrace() + return ResponseEntity.status(INTERNAL_SERVER_ERROR) + .body([jsonParseError : e.getMessage(), + sourceUiSchemaDefinitionFile: this.jsonSchemaLocation.url]) + } + } + + @PostConstruct + void init() { + this.jsonSchemaLocation = entityAttributesFiltersSchema(this.jsonSchemaResourceLocationRegistry); + } +} 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/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 2423c48f5..d138f3a57 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -1,14 +1,20 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry +import edu.internet2.tier.shibboleth.admin.ui.service.JsonSchemaBuilderService +import org.slf4j.Logger +import org.slf4j.LoggerFactory 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.RequestMapping import org.springframework.web.bind.annotation.RestController +import javax.annotation.PostConstruct + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR /** @@ -22,29 +28,38 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR @RequestMapping('/api/ui/MetadataSources') class MetadataSourcesUiDefinitionController { + private static final Logger logger = LoggerFactory.getLogger(MetadataSourcesUiDefinitionController.class); + @Autowired - MetadataSourcesJsonSchemaResourceLocation jsonSchemaLocation + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + JsonSchemaResourceLocation jsonSchemaLocation @Autowired ObjectMapper jacksonObjectMapper @Autowired - CustomAttributesConfiguration customAttributesConfiguration + JsonSchemaBuilderService jsonSchemaBuilderService @GetMapping ResponseEntity getUiDefinitionJsonSchema() { try { def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) - parsedJson['properties']['attributeRelease']['widget']['data'] = - customAttributesConfiguration.getAttributes().collect { - [key: it['name'], label: it['displayName']] - } + jsonSchemaBuilderService.addReleaseAttributesToJson(parsedJson['properties']['attributeRelease']['widget']) + jsonSchemaBuilderService.addRelyingPartyOverridesToJson(parsedJson['properties']['relyingPartyOverrides']) + jsonSchemaBuilderService.addRelyingPartyOverridesCollectionDefinitionsToJson(parsedJson["definitions"]) return ResponseEntity.ok(parsedJson) } - catch (Exception e) { + catch (IOException e) { + logger.error("An error occurred while attempting to get json schema for metadata sources!", e) return ResponseEntity.status(INTERNAL_SERVER_ERROR) .body([jsonParseError : e.getMessage(), sourceUiSchemaDefinitionFile: this.jsonSchemaLocation.url]) } } + + @PostConstruct + void init() { + this.jsonSchemaLocation = metadataSourcesSchema(this.jsonSchemaResourceLocationRegistry); + } } diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy new file mode 100644 index 000000000..712142172 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy @@ -0,0 +1,61 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import mjson.Json +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.MethodParameter +import org.springframework.http.HttpInputMessage +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.context.request.WebRequest +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter + +import javax.annotation.PostConstruct +import java.lang.reflect.Type + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema + +/** + * Controller advice implementation for validating relying party overrides payload coming from UI layer + * against pre-defined JSON schema. + * + * @author Dmitriy Kopylenko + */ +@ControllerAdvice +class RelyingPartyOverridesJsonSchemaValidatingControllerAdvice extends RequestBodyAdviceAdapter { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + JsonSchemaResourceLocation jsonSchemaLocation + + @Override + boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + targetType.typeName == EntityDescriptorRepresentation.typeName + } + + @Override + Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) { + def relyingPartyOverrides = EntityDescriptorRepresentation.cast(body).relyingPartyOverrides + def relyingPartyOverridesJson = Json.make([relyingPartyOverrides: relyingPartyOverrides]) + def schema = Json.schema(this.jsonSchemaLocation.uri) + def validationResult = schema.validate(relyingPartyOverridesJson) + if (!validationResult.at('ok')) { + throw new JsonSchemaValidationFailedException(validationResult.at('errors').asList()) + } + body + } + + @ExceptionHandler(JsonSchemaValidationFailedException) + final ResponseEntity handleUserNotFoundException(JsonSchemaValidationFailedException ex, WebRequest request) { + new ResponseEntity<>([errors: ex.errors], HttpStatus.BAD_REQUEST) + } + + @PostConstruct + void init() { + this.jsonSchemaLocation = metadataSourcesSchema(this.jsonSchemaResourceLocationRegistry); + } +} diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 4e6879d0f..4a95484e8 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -125,15 +125,15 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) - if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { - constructXmlNodeForResolver(mr, delegate) { - //TODO: enhance - mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - constructXmlNodeForFilter(filter, delegate) + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + //TODO: enhance + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + constructXmlNodeForFilter(filter, delegate) + } } } - } } } return DOMBuilder.newInstance().parseText(writer.toString()) @@ -407,4 +407,4 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } -} +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy new file mode 100644 index 000000000..b98bcab26 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import org.springframework.beans.factory.annotation.Autowired + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class JsonSchemaBuilderService { + + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + + void addReleaseAttributesToJson(Object json) { + json['data'] = customPropertiesConfiguration.getAttributes().collect { + [key: it['name'], label: it['displayName']] + } + } + + void addRelyingPartyOverridesToJson(Object json) { + def properties = [:] + customPropertiesConfiguration.getOverrides().each { + def property + if (it['displayType'] == 'list' + || it['displayType'] == 'set') { + property = [$ref: '#/definitions/' + it['name']] + } else { + property = + [title : it['displayName'], + description: it['helpText'], + type : it['displayType']] + if (it['displayType'] == 'boolean') { + property['default'] = (Boolean)(it['defaultValue']) + } else { + property['default'] = it['defaultValue'] + } + } + properties[(String) it['name']] = property + } + json['properties'] = properties + } + + void addRelyingPartyOverridesCollectionDefinitionsToJson(Object json) { + customPropertiesConfiguration.getOverrides().stream().filter { + it -> it['displayType'] && (it['displayType'] == 'list' || it['displayType'] == 'set') + }.each { + def definition = [title : it['displayName'], + description: it['helpText'], + type : 'array', + default : null] + if (it['displayType'] == 'set') { + definition['uniqueItems'] = true + } else if (it['displayType'] == 'list') { + definition['uniqueItems'] = false + } + def items = [type : 'string', + minLength: 1, // TODO: should this be configurable? + maxLength: 255] //TODO: or this? + items.widget = [id: 'datalist', data: it['defaultValues']] + + definition['items'] = items + json[(String) it['name']] = definition + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index a63c3da6e..fc1e0e8b4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -25,13 +25,14 @@ import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; +import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import org.apache.lucene.analysis.Analyzer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; @@ -46,7 +47,7 @@ import javax.servlet.http.HttpServletRequest; @Configuration -@EnableConfigurationProperties(CustomAttributesConfiguration.class) +@EnableConfigurationProperties(CustomPropertiesConfiguration.class) public class CoreShibUiConfiguration { private static final Logger logger = LoggerFactory.getLogger(CoreShibUiConfiguration.class); @@ -180,12 +181,17 @@ public LuceneUtility luceneUtility(DirectoryService directoryService) { } @Bean - public CustomAttributesConfiguration customAttributesConfiguration() { - return new CustomAttributesConfiguration(); + public CustomPropertiesConfiguration customPropertiesConfiguration() { + return new CustomPropertiesConfiguration(); } @Bean public Module stringTrimModule() { return new StringTrimModule(); } + + @Bean + public ModelRepresentationConversions modelRepresentationConversions() { + return new ModelRepresentationConversions(customPropertiesConfiguration()); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java similarity index 62% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java index aa12be6b2..a6a1db63d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -12,9 +13,10 @@ */ @Configuration @ConfigurationProperties(prefix="custom") -public class CustomAttributesConfiguration { +public class CustomPropertiesConfiguration { private List> attributes = new ArrayList<>(); + private List overrides = new ArrayList<>(); public List> getAttributes() { return attributes; @@ -23,4 +25,12 @@ public List> getAttributes() { public void setAttributes(List> attributes) { this.attributes = attributes; } + + public List getOverrides() { + return overrides; + } + + public void setOverrides(List overrides) { + this.overrides = overrides; + } } 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 new file mode 100644 index 000000000..4c4ad86db --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -0,0 +1,91 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry; +import edu.internet2.tier.shibboleth.admin.ui.service.JsonSchemaBuilderService; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; + +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 + */ +@Configuration +@ConfigurationProperties("shibui") +public class JsonSchemaComponentsConfiguration { + + //Configured via @ConfigurationProperties (using setter method) with 'shibui.metadata-sources-ui-schema-location' property and default + //value set here if that property is not explicitly set in application.properties + @Setter + private String metadataSourcesUiSchemaLocation = "classpath:metadata-sources-ui-schema.json"; + + //Configured via @ConfigurationProperties (using setter method) with 'shibui.entity-attributes-filters-ui-schema-location' property and + // default value set here if that property is not explicitly set in application.properties + @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() + .register(METADATA_SOURCES, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(metadataSourcesUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) + .build()) + .register(ENTITY_ATTRIBUTES_FILTERS, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(entityAttributesFiltersUiSchemaLocation) + .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());*/ + + } + + @Bean + public JsonSchemaBuilderService jsonSchemaBuilderService() { + return new JsonSchemaBuilderService(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java deleted file mode 100644 index 48fb33ede..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.configuration; - -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ResourceLoader; - -/** - * @author Dmitriy Kopylenko - */ -@Configuration -@ConfigurationProperties("shibui") -public class JsonSchemaValidationComponentsConfiguration { - - //Configured via @ConfigurationProperties (using setter method) with 'shibui.metadata-sources-ui-schema-location' property and default - //value set here if that property is not explicitly set in application.properties - private String metadataSourcesUiSchemaLocation ="classpath:metadata-sources-ui-schema.json"; - - //This setter is used by Boot's @ConfiguratonProperties binding machinery - public void setMetadataSourcesUiSchemaLocation(String metadataSourcesUiSchemaLocation) { - this.metadataSourcesUiSchemaLocation = metadataSourcesUiSchemaLocation; - } - - @Bean - public MetadataSourcesJsonSchemaResourceLocation metadataSourcesJsonSchemaResourceLocation(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { - return new MetadataSourcesJsonSchemaResourceLocation(metadataSourcesUiSchemaLocation, resourceLoader, jacksonMapper); - } -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java index 4a0388428..f71e76cb5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java @@ -1,6 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration; +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -15,10 +15,10 @@ public class ConfigurationController { @Autowired - CustomAttributesConfiguration customAttributesConfiguration; + CustomPropertiesConfiguration customPropertiesConfiguration; @GetMapping(value = "/customAttributes") public ResponseEntity getCustomAttributes() { - return ResponseEntity.ok(customAttributesConfiguration.getAttributes()); + return ResponseEntity.ok(customPropertiesConfiguration.getAttributes()); } } 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/RelyingPartyOverrideProperty.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java new file mode 100644 index 000000000..24432d1be --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java @@ -0,0 +1,40 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Setter +@Getter +public class RelyingPartyOverrideProperty { + private String name; + private String displayName; + private String displayType; + private String defaultValue; + private String helpText; + private List defaultValues; + private String persistType; + private String persistValue; + private String attributeName; + private String attributeFriendlyName; + + @Override + public String toString() { + return "RelyingPartyOverrideProperty{" + + "\nname='" + name + '\'' + + ", \ndisplayName='" + displayName + '\'' + + ", \ndisplayType='" + displayType + '\'' + + ", \ndefaultValue='" + defaultValue + '\'' + + ", \nhelpText='" + helpText + '\'' + + ", \npersistType='" + persistType + '\'' + + ", \npersistValue='" + persistValue + '\'' + + ", \ndefaultValues=" + defaultValues + + ", \nattributeName='" + attributeName + '\'' + + ", \nattributeFriendlyName='" + attributeFriendlyName + '\'' + + "\n}"; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java index 8492e745d..86a2181c7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -16,8 +15,8 @@ import javax.persistence.PostLoad; import javax.persistence.Transient; import java.util.ArrayList; - import java.util.List; +import java.util.Map; import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromAttributeReleaseList; import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromRelyingPartyOverridesRepresentation; @@ -52,9 +51,9 @@ public void setAttributeRelease(List attributeRelease) { } @Transient - private RelyingPartyOverridesRepresentation relyingPartyOverrides; + private Map relyingPartyOverrides; - public void setRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + public void setRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { this.relyingPartyOverrides = relyingPartyOverridesRepresentation; this.rebuildAttributes(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index c684d458a..b879e76d5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class EntityDescriptorRepresentation implements Serializable { @@ -58,7 +59,7 @@ public EntityDescriptorRepresentation(String id, private LocalDateTime modifiedDate; - private RelyingPartyOverridesRepresentation relyingPartyOverrides; + private Map relyingPartyOverrides; private List attributeRelease; @@ -180,11 +181,11 @@ public void setModifiedDate(LocalDateTime modifiedDate) { this.modifiedDate = modifiedDate; } - public RelyingPartyOverridesRepresentation getRelyingPartyOverrides() { + public Map getRelyingPartyOverrides() { return relyingPartyOverrides; } - public void setRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverrides) { + public void setRelyingPartyOverrides(Map relyingPartyOverrides) { this.relyingPartyOverrides = relyingPartyOverrides; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java index b9e2f1213..0276a2b9c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java @@ -3,13 +3,14 @@ import java.io.Serializable; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; public class FilterRepresentation implements Serializable { private String id; private String filterName; private boolean filterEnabled; private FilterTargetRepresentation filterTarget; - private RelyingPartyOverridesRepresentation relyingPartyOverrides; + private Map relyingPartyOverrides; private List attributeRelease; private LocalDateTime createdDate; private LocalDateTime modifiedDate; @@ -57,11 +58,11 @@ public void setFilterTarget(FilterTargetRepresentation filterTarget) { this.filterTarget = filterTarget; } - public RelyingPartyOverridesRepresentation getRelyingPartyOverrides() { + public Map getRelyingPartyOverrides() { return relyingPartyOverrides; } - public void setRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverrides) { + public void setRelyingPartyOverrides(Map relyingPartyOverrides) { this.relyingPartyOverrides = relyingPartyOverrides; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java deleted file mode 100644 index cac2a1d6a..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/RelyingPartyOverridesRepresentation.java +++ /dev/null @@ -1,110 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -public class RelyingPartyOverridesRepresentation implements Serializable { - - private static final long serialVersionUID = 2439457246884861580L; - - private boolean signAssertion; - - private boolean dontSignResponse; - - private boolean turnOffEncryption; - - private boolean useSha; - - private boolean ignoreAuthenticationMethod; - - private boolean omitNotBefore; - - private String responderId; - - private List nameIdFormats = new ArrayList<>(); - - private List authenticationMethods = new ArrayList<>(); - - private boolean forceAuthn; - - public boolean isSignAssertion() { - return signAssertion; - } - - public void setSignAssertion(boolean signAssertion) { - this.signAssertion = signAssertion; - } - - public boolean isDontSignResponse() { - return dontSignResponse; - } - - public void setDontSignResponse(boolean dontSignResponse) { - this.dontSignResponse = dontSignResponse; - } - - public boolean isTurnOffEncryption() { - return turnOffEncryption; - } - - public void setTurnOffEncryption(boolean turnOffEncryption) { - this.turnOffEncryption = turnOffEncryption; - } - - public boolean isUseSha() { - return useSha; - } - - public void setUseSha(boolean useSha) { - this.useSha = useSha; - } - - public boolean isIgnoreAuthenticationMethod() { - return ignoreAuthenticationMethod; - } - - public void setIgnoreAuthenticationMethod(boolean ignoreAuthenticationMethod) { - this.ignoreAuthenticationMethod = ignoreAuthenticationMethod; - } - - public boolean isOmitNotBefore() { - return omitNotBefore; - } - - public void setOmitNotBefore(boolean omitNotBefore) { - this.omitNotBefore = omitNotBefore; - } - - public String getResponderId() { - return responderId; - } - - public void setResponderId(String responderId) { - this.responderId = responderId; - } - - public List getNameIdFormats() { - return nameIdFormats; - } - - public void setNameIdFormats(List nameIdFormats) { - this.nameIdFormats = nameIdFormats; - } - - public List getAuthenticationMethods() { - return authenticationMethods; - } - - public void setAuthenticationMethods(List authenticationMethods) { - this.authenticationMethods = authenticationMethods; - } - - public boolean isForceAuthn() { - return forceAuthn; - } - - public void setForceAuthn(boolean forceAuthn) { - this.forceAuthn = forceAuthn; - } -} 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/InMemoryJsonSchemaResourceLocationRegistry.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/InMemoryJsonSchemaResourceLocationRegistry.java new file mode 100644 index 000000000..2840619b2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/InMemoryJsonSchemaResourceLocationRegistry.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; + +/** + * Default implementation of {@link JsonSchemaResourceLocationRegistry}. + *

+ * This class has package private visibility as creation of it is delegated to public static factory method + * on the registry interface itself. + * + * @author Dmitriy Kopylenko + */ +class InMemoryJsonSchemaResourceLocationRegistry implements JsonSchemaResourceLocationRegistry { + + private Map schemaLocations = + new EnumMap<>(JsonSchemaResourceLocation.SchemaType.class); + + + @Override + public JsonSchemaResourceLocationRegistry register(JsonSchemaResourceLocation.SchemaType type, JsonSchemaResourceLocation location) { + this.schemaLocations.put(type, location); + return this; + } + + @Override + public Optional lookup(JsonSchemaResourceLocation.SchemaType type) { + return Optional.ofNullable(this.schemaLocations.get(type)); + } +} 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 new file mode 100644 index 000000000..0083450b3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java @@ -0,0 +1,66 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +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. + * + * @author Dmitriy Kopylenko + */ +public abstract class JsonSchemaLocationLookup { + + /** + * Searches metadata sources JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return metadata sources JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + public static JsonSchemaResourceLocation metadataSourcesSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry + .lookup(METADATA_SOURCES) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for metadata sources is not registered.")); + } + + /** + * Searches entity attributes filters JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @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) { + return resourceLocationRegistry + .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 new file mode 100644 index 000000000..05280d45e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java @@ -0,0 +1,134 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Builder; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.core.io.ResourceLoader; + +import javax.annotation.PostConstruct; +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. + * + * @author Dmitriy Kopylenko + */ +public class JsonSchemaResourceLocation { + + private final String jsonSchemaLocation; + + private URL jsonSchemaUrl; + + private final ResourceLoader resourceLoader; + + private final ObjectMapper jacksonMapper; + + private boolean detectMalformedJsonDuringInit = true; + + public JsonSchemaResourceLocation(String jsonSchemaLocation, ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { + this.jsonSchemaLocation = jsonSchemaLocation; + this.resourceLoader = resourceLoader; + this.jacksonMapper = jacksonMapper; + } + + //This constructor is used in tests + public JsonSchemaResourceLocation(String jsonSchemaLocation, + ResourceLoader resourceLoader, + ObjectMapper jacksonMapper, + boolean detectMalformedJsonDuringInit) { + + this.jsonSchemaLocation = jsonSchemaLocation; + this.resourceLoader = resourceLoader; + this.jacksonMapper = jacksonMapper; + this.detectMalformedJsonDuringInit = detectMalformedJsonDuringInit; + } + + public URL getUrl() { + return this.jsonSchemaUrl; + } + + public URI getUri() { + try { + return this.jsonSchemaUrl.toURI(); + } + catch (URISyntaxException ex) { + throw new RuntimeException(ex); + } + } + + @PostConstruct + public void init() { + try { + this.jsonSchemaUrl = this.resourceLoader.getResource(this.jsonSchemaLocation).getURL(); + if(this.detectMalformedJsonDuringInit) { + //Detect malformed JSON schema early, during application start up and fail fast with useful exception message + this.jacksonMapper.readValue(this.jsonSchemaUrl, Map.class); + } + } + catch (Exception ex) { + StringBuilder msg = + new StringBuilder(String.format("An error is detected during JSON parsing => [%s]", ex.getMessage())); + msg.append(String.format("Offending resource => [%s]", this.jsonSchemaLocation)); + + throw new BeanInitializationException(msg.toString(), ex); + } + } + + public static class JsonSchemaLocationBuilder { + + @Builder(builderMethodName = "with") + public static JsonSchemaResourceLocation newSchemaLocation(String jsonSchemaLocation, + ResourceLoader resourceLoader, + ObjectMapper jacksonMapper, + boolean detectMalformedJson) { + JsonSchemaResourceLocation location = new JsonSchemaResourceLocation(jsonSchemaLocation, resourceLoader, jacksonMapper, detectMalformedJson); + location.init(); + return location; + } + } + + public enum SchemaType { + // 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/jsonschema/JsonSchemaResourceLocationRegistry.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocationRegistry.java new file mode 100644 index 000000000..e5a6e88c5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocationRegistry.java @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +import java.util.Optional; + +/** + * An API to store and expose JSON schema resource locations for various JSON schema types. Typically configured as a Spring + * bean and injected into Spring-managed components interested in looking up JSON schema locations by particular type. + * + * @author Dmitriy Kopylenko + */ +public interface JsonSchemaResourceLocationRegistry { + + /** + * Register json schema resource location for given schema type. + * + * @param type of JSON schema + * @param location of JSON schema resource + */ + JsonSchemaResourceLocationRegistry register(JsonSchemaResourceLocation.SchemaType type, JsonSchemaResourceLocation location); + + /** + * Look up json schema resource location by given schema type. + * + * @param type type of JSON schema + * @return optional location of JSON schema resource + */ + Optional lookup(JsonSchemaResourceLocation.SchemaType type); + + /** + * Factory method. + * + * @return in-memory implementation + */ + static JsonSchemaResourceLocationRegistry inMemory() { + return new InMemoryJsonSchemaResourceLocationRegistry(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java new file mode 100644 index 000000000..eac2a6d2f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +import java.util.List; + +/** + * Indicates JSON schema validation failure. Encapsulates a list of error messages produced by JSON schema validator + * component. + * + * @author Dmitriy Kopylenko + */ +class JsonSchemaValidationFailedException extends RuntimeException { + + List errors; + + JsonSchemaValidationFailedException(List errors) { + this.errors = errors; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java deleted file mode 100644 index 98d53cd39..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java +++ /dev/null @@ -1,77 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.jsonschema; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.core.io.ResourceLoader; - -import javax.annotation.PostConstruct; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Map; - -/** - * Encapsulates metadata sources JSON schema location. - * - * @author Dmitriy Kopylenko - */ -public class MetadataSourcesJsonSchemaResourceLocation { - - private final String metadataSourcesUiSchemaLocation; - - private URL jsonSchemaUrl; - - private final ResourceLoader resourceLoader; - - private final ObjectMapper jacksonMapper; - - private boolean detectMalformedJsonDuringInit = true; - - public MetadataSourcesJsonSchemaResourceLocation(String metadataSourcesUiSchemaLocation, ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { - this.metadataSourcesUiSchemaLocation = metadataSourcesUiSchemaLocation; - this.resourceLoader = resourceLoader; - this.jacksonMapper = jacksonMapper; - } - - //This constructor is used in tests - public MetadataSourcesJsonSchemaResourceLocation(String metadataSourcesUiSchemaLocation, - ResourceLoader resourceLoader, - ObjectMapper jacksonMapper, - boolean detectMalformedJsonDuringInit) { - this.metadataSourcesUiSchemaLocation = metadataSourcesUiSchemaLocation; - this.resourceLoader = resourceLoader; - this.jacksonMapper = jacksonMapper; - this.detectMalformedJsonDuringInit = detectMalformedJsonDuringInit; - } - - public URL getUrl() { - return this.jsonSchemaUrl; - } - - public URI getUri() { - try { - return this.jsonSchemaUrl.toURI(); - } - catch (URISyntaxException ex) { - throw new RuntimeException(ex); - } - } - - @PostConstruct - public void init() { - try { - this.jsonSchemaUrl = this.resourceLoader.getResource(this.metadataSourcesUiSchemaLocation).getURL(); - if(this.detectMalformedJsonDuringInit) { - //Detect malformed JSON schema early, during application start up and fail fast with useful exception message - this.jacksonMapper.readValue(this.jsonSchemaUrl, Map.class); - } - } - catch (Exception ex) { - StringBuilder msg = - new StringBuilder(String.format("An error is detected during JSON parsing => [%s]", ex.getMessage())); - msg.append(String.format("Offending resource => [%s]", this.metadataSourcesUiSchemaLocation)); - - throw new BeanInitializationException(msg.toString(), ex); - } - } -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 086c65f64..ea57c4d06 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -2,10 +2,10 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import java.util.List; +import java.util.Map; /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityDescriptor} state. @@ -47,11 +47,11 @@ public interface EntityDescriptorService { List getAttributeReleaseListFromAttributeList(List attributeList); /** - * Given a list of attributes, generate a RelyingPartyOverridesRepresentation + * Given a list of attributes, generate a map of relying party overrides * * @param attributeList the list of attributes to generate from - * @return a RelyingPartyOverridesRepresentation based on the given list of attributes + * @return a map of String->Object (property name -> property value) based on the given list of attributes */ - RelyingPartyOverridesRepresentation getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); + Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java index 5a4a65a25..6a676844a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java @@ -1,10 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import org.opensaml.saml.saml2.core.Attribute; import java.util.List; +import java.util.Map; /** * facade API that defines operations for creating various entities from JSON representations @@ -13,5 +13,5 @@ public interface EntityService { List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation); edu.internet2.tier.shibboleth.admin.ui.domain.Attribute getAttributeFromAttributeReleaseList(List attributeReleaseList); List getAttributeListFromAttributeReleaseList(List attributeReleaseList); - List getAttributeListFromRelyingPartyOverridesRepresentation(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation); + List getAttributeListFromRelyingPartyOverridesRepresentation(Map relyingPartyOverridesRepresentation); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index a0ec7e1b6..adb8e4cb8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -25,25 +25,28 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName; import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL; import edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL; +import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService; import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.schema.XSBooleanValue; import org.opensaml.xmlsec.signature.KeyInfo; import org.opensaml.xmlsec.signature.X509Certificate; @@ -54,12 +57,13 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getBooleanValueOfAttribute; import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListValueOfAttribute; /** * Default implementation of {@link EntityDescriptorService} @@ -489,66 +493,75 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope // set up extensions if (ed.getExtensions() != null && ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME) != null && ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).size() == 1) { // we have entity attributes (hopefully), so should have overrides - RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = new RelyingPartyOverridesRepresentation(); - representation.setRelyingPartyOverrides(relyingPartyOverridesRepresentation); + Map relyingPartyOverrides = new HashMap<>(); for (org.opensaml.saml.saml2.core.Attribute attribute : ((EntityAttributes) ed.getExtensions().getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).get(0)).getAttributes()) { Attribute jpaAttribute = (Attribute) attribute; - // TODO: this is going to get real ugly real quick. clean it up, future Jj! - switch (jpaAttribute.getName()) { - case MDDCConstants.SIGN_ASSERTIONS: - relyingPartyOverridesRepresentation.setSignAssertion(getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.SIGN_RESPONSES: - relyingPartyOverridesRepresentation.setDontSignResponse(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.ENCRYPT_ASSERTIONS: - relyingPartyOverridesRepresentation.setTurnOffEncryption(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.SECURITY_CONFIGURATION: - if (getStringListValueOfAttribute(jpaAttribute).contains("shibboleth.SecurityConfiguration.SHA1")) { - relyingPartyOverridesRepresentation.setUseSha(true); - } - break; - case MDDCConstants.DISALLOWED_FEATURES: - if ((Integer.decode(getStringListValueOfAttribute(jpaAttribute).get(0)) & 0x1) == 0x1) { - relyingPartyOverridesRepresentation.setIgnoreAuthenticationMethod(true); + + if (jpaAttribute.getName().equals(MDDCConstants.RELEASE_ATTRIBUTES)) { + representation.setAttributeRelease(getStringListOfAttributeValues(attribute.getAttributeValues())); + } else { + Optional override = ModelRepresentationConversions.getOverrideByAttributeName(jpaAttribute.getName()); + if (override.isPresent()) { + RelyingPartyOverrideProperty overrideProperty = (RelyingPartyOverrideProperty)override.get(); + Object attributeValues = null; + switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { + case STRING: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected!"); + } + attributeValues = getValueFromXSStringOrXSAny(jpaAttribute.getAttributeValues().get(0)); + break; + case INTEGER: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected!"); + } + attributeValues = ((XSInteger)jpaAttribute.getAttributeValues().get(0)).getValue(); + break; + case BOOLEAN: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected!"); + } + if (overrideProperty.getPersistType() != null && + !overrideProperty.getPersistType().equals(overrideProperty.getDisplayType())) { + attributeValues = getValueFromXSStringOrXSAny(jpaAttribute.getAttributeValues().get(0)); + } else { + attributeValues = Boolean.valueOf(((XSBoolean) jpaAttribute.getAttributeValues() + .get(0)).getStoredValue()); + } + break; + case SET: + case LIST: + attributeValues = jpaAttribute.getAttributeValues().stream() + .map(attributeValue -> getValueFromXSStringOrXSAny(attributeValue)) + .collect(Collectors.toList()); } - break; - case MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE: - relyingPartyOverridesRepresentation.setOmitNotBefore(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.RESPONDER_ID: - relyingPartyOverridesRepresentation.setResponderId(getStringListValueOfAttribute(jpaAttribute).get(0)); - break; - case MDDCConstants.NAME_ID_FORMAT_PRECEDENCE: - relyingPartyOverridesRepresentation.setNameIdFormats(getStringListValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.DEFAULT_AUTHENTICATION_METHODS: - relyingPartyOverridesRepresentation.setAuthenticationMethods(getStringListValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.RELEASE_ATTRIBUTES: - representation.setAttributeRelease(getStringListOfAttributeValues(attribute.getAttributeValues())); - break; - case MDDCConstants.FORCE_AUTHN: - relyingPartyOverridesRepresentation.setForceAuthn(getBooleanValueOfAttribute(jpaAttribute)); - break; - default: - break; + relyingPartyOverrides.put(((RelyingPartyOverrideProperty) override.get()).getName(), attributeValues); + } } } + + representation.setRelyingPartyOverrides(relyingPartyOverrides); } return representation; } + private String getValueFromXSStringOrXSAny(XMLObject xmlObject) { + if (xmlObject instanceof XSAny) { + return ((XSAny)xmlObject).getTextContent(); + } else { + return ((XSString)xmlObject).getValue(); + } + } + @Override public List getAttributeReleaseListFromAttributeList(List attributeList) { return ModelRepresentationConversions.getAttributeReleaseListFromAttributeList(attributeList); } @Override - public RelyingPartyOverridesRepresentation getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { + public Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java index e8b4ce590..31a8b28ae 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java @@ -1,18 +1,21 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; +import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; +import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import org.opensaml.saml.saml2.core.Attribute; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class JPAEntityServiceImpl implements EntityService { @@ -22,6 +25,9 @@ public class JPAEntityServiceImpl implements EntityService { @Autowired private AttributeUtility attributeUtility; + @Autowired + private CustomPropertiesConfiguration customPropertiesConfiguration; + public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects) { this.openSamlObjects = openSamlObjects; } @@ -31,6 +37,14 @@ public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects, AttributeUtility at this.attributeUtility = attributeUtility; } + public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects, + AttributeUtility attributeUtility, + CustomPropertiesConfiguration customPropertiesConfiguration) { + this.openSamlObjects = openSamlObjects; + this.attributeUtility = attributeUtility; + this.customPropertiesConfiguration = customPropertiesConfiguration; + } + @Override public List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation) { List list = new ArrayList<>(); @@ -81,44 +95,51 @@ public List getAttributeListFromAttributeReleaseList(List att } @Override - public List getAttributeListFromRelyingPartyOverridesRepresentation(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + public List getAttributeListFromRelyingPartyOverridesRepresentation(Map relyingPartyOverridesRepresentation) { + List overridePropertyList = customPropertiesConfiguration.getOverrides(); List list = new ArrayList<>(); - if (relyingPartyOverridesRepresentation != null) { - if (relyingPartyOverridesRepresentation.isSignAssertion()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); - } - if (relyingPartyOverridesRepresentation.isDontSignResponse()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); - } - if (relyingPartyOverridesRepresentation.isTurnOffEncryption()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); - } - if (relyingPartyOverridesRepresentation.isUseSha()) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); - } - if (relyingPartyOverridesRepresentation.isIgnoreAuthenticationMethod()) { - // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate - // TODO: fix - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); - } - if (relyingPartyOverridesRepresentation.isOmitNotBefore()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); - } - if (relyingPartyOverridesRepresentation.getResponderId() != null && !"".equals(relyingPartyOverridesRepresentation.getResponderId())) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, relyingPartyOverridesRepresentation.getResponderId())); - } - if (relyingPartyOverridesRepresentation.getNameIdFormats() != null && relyingPartyOverridesRepresentation.getNameIdFormats().size() > 0) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, relyingPartyOverridesRepresentation.getNameIdFormats())); - } - if (relyingPartyOverridesRepresentation.getAuthenticationMethods() != null && relyingPartyOverridesRepresentation.getAuthenticationMethods().size() > 0) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, relyingPartyOverridesRepresentation.getAuthenticationMethods())); - } - if (relyingPartyOverridesRepresentation.isForceAuthn()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.FORCE_AUTHN, MDDCConstants.FORCE_AUTHN_FN, true)); + for (Map.Entry entry : relyingPartyOverridesRepresentation.entrySet()) { + String key = (String) entry.getKey(); + RelyingPartyOverrideProperty overrideProperty = overridePropertyList.stream().filter(op -> op.getName().equals(key)).findFirst().get(); + switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { + case BOOLEAN: + if (overrideProperty.getPersistType() != null && + !overrideProperty.getPersistType().equalsIgnoreCase("boolean")) { + list.add(attributeUtility.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (String) entry.getValue())); + } else { + list.add(attributeUtility.createAttributeWithBooleanValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (Boolean) entry.getValue())); + } + break; + case INTEGER: + list.add(attributeUtility.createAttributeWithIntegerValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + Integer.valueOf((String) entry.getValue()))); + break; + case STRING: + list.add(attributeUtility.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (String) entry.getValue())); + break; + case SET: + list.add(attributeUtility.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (List) entry.getValue())); + break; + case LIST: + list.add(attributeUtility.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (List) entry.getValue())); + break; + default: + throw new UnsupportedOperationException("getAttributeListFromRelyingPartyOverridesRepresentation was called with an unsupported type (" + overrideProperty.getDisplayType() + ")!"); } } - return (List)(List)list; + return (List) (List) list; } } 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/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java index 2282b04e2..20fca363c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java @@ -3,12 +3,13 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import org.opensaml.core.xml.schema.XSBooleanValue; -import org.springframework.beans.factory.annotation.Autowired; import java.util.List; +import java.util.Set; /** * @author Bill Smith (wsmith@unicon.net) @@ -17,54 +18,66 @@ public class AttributeUtility { private OpenSamlObjects openSamlObjects; + private static final String URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; + public AttributeUtility(OpenSamlObjects openSamlObjects) { this.openSamlObjects = openSamlObjects; } public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithBooleanValue(String name, String friendlyName, Boolean value) { - edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - attribute.setName(name); - attribute.setFriendlyName(friendlyName); - attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = createNewAttribute(name, friendlyName); XSBoolean xsBoolean = (XSBoolean) openSamlObjects.getBuilderFactory().getBuilder(XSBoolean.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSBoolean.TYPE_NAME); xsBoolean.setValue(XSBooleanValue.valueOf(value.toString())); - attribute.getAttributeValues().add(xsBoolean); + return attribute; } - public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithStringValues(String name, List values) { - edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory() - .getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - attribute.setName(name); - //TODO: Do we need a friendlyName? - //TODO: Do we need a NameFormat? + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithIntegerValue(String name, String friendlyName, Integer value) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = createNewAttribute(name, friendlyName); + + XSInteger xsInteger = (XSInteger) openSamlObjects.getBuilderFactory().getBuilder(XSInteger.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSInteger.TYPE_NAME); + xsInteger.setValue(value); + attribute.getAttributeValues().add(xsInteger); - values.forEach(attributeString -> { - XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); - xsString.setValue(attributeString); - attribute.getAttributeValues().add(xsString); - }); return attribute; } - public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithArbitraryValues(String name, String friendlyName, String... values) { - edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - attribute.setName(name); - attribute.setFriendlyName(friendlyName); - attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithStringValues(String name, String friendlyName, String... values) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = createNewAttribute(name, friendlyName); for (String value : values) { - XSAny xsAny = (XSAny) openSamlObjects.getBuilderFactory().getBuilder(XSAny.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); - xsAny.setTextContent(value); - attribute.getAttributeValues().add(xsAny); + XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + xsString.setValue(value); + attribute.getAttributeValues().add(xsString); } return attribute; } - public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithArbitraryValues(String name, String friendlyName, List values) { - return createAttributeWithArbitraryValues(name, friendlyName, values.toArray(new String[]{})); + /* + * Provided for calling with name = MDDCConstants.RELEASE_ATTRIBUTES. + */ + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithStringValues(String name, List values) { + return createAttributeWithStringValues(name, null, values); + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithStringValues(String name, String friendlyName, List values) { + return createAttributeWithStringValues(name, friendlyName, values.toArray(new String[]{})); + } + + /* Calling this method with name = MDDCConstants.RELEASE_ATTRIBUTES seems to be a special case. In this case, + * we haven't been setting the friendlyName or nameFormat. Hence the null check. + */ + private edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createNewAttribute(String name, String friendlyName) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory() + .getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + if (friendlyName != null) { + attribute.setFriendlyName(friendlyName); + attribute.setNameFormat(URI); + } + return attribute; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java index d2fbda436..f7279179c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java @@ -1,23 +1,40 @@ package edu.internet2.tier.shibboleth.admin.util; +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import org.opensaml.core.xml.XMLObject; +import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** * Utility class to deal with model conversions related functionality */ -public abstract class ModelRepresentationConversions { +public class ModelRepresentationConversions { private static final AttributeUtility ATTRIBUTE_UTILITY; + private static CustomPropertiesConfiguration customPropertiesConfiguration; + + @Autowired + public ModelRepresentationConversions(CustomPropertiesConfiguration customPropertiesConfiguration) { + ModelRepresentationConversions.customPropertiesConfiguration = customPropertiesConfiguration; + } + static { OpenSamlObjects openSamlObjects = new OpenSamlObjects(); try { @@ -62,53 +79,53 @@ public static List getStringListValueOfAttribute(Attribute attribute) { return getStringListOfAttributeValues(attribute.getAttributeValues()); } - public static RelyingPartyOverridesRepresentation getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { - RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = new RelyingPartyOverridesRepresentation(); + public static Optional getOverrideByAttributeName(String attributeName) { + return customPropertiesConfiguration.getOverrides().stream().filter(it -> it.getAttributeName().equals(attributeName)).findFirst(); + } + + public static Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { + Map relyingPartyOverrides = new HashMap<>(); for (org.opensaml.saml.saml2.core.Attribute attribute : attributeList) { Attribute jpaAttribute = (Attribute) attribute; - // TODO: this is going to get real ugly real quick. clean it up, future Jj! - switch (jpaAttribute.getName()) { - case MDDCConstants.SIGN_ASSERTIONS: - relyingPartyOverridesRepresentation.setSignAssertion(getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.SIGN_RESPONSES: - relyingPartyOverridesRepresentation.setDontSignResponse(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.ENCRYPT_ASSERTIONS: - relyingPartyOverridesRepresentation.setTurnOffEncryption(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.SECURITY_CONFIGURATION: - if (getStringListValueOfAttribute(jpaAttribute).contains("shibboleth.SecurityConfiguration.SHA1")) { - relyingPartyOverridesRepresentation.setUseSha(true); - } - break; - case MDDCConstants.DISALLOWED_FEATURES: - if ((Integer.decode(getStringListValueOfAttribute(jpaAttribute).get(0)) & 0x1) == 0x1) { - relyingPartyOverridesRepresentation.setIgnoreAuthenticationMethod(true); - } - break; - case MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE: - relyingPartyOverridesRepresentation.setOmitNotBefore(!getBooleanValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.RESPONDER_ID: - relyingPartyOverridesRepresentation.setResponderId(getStringListValueOfAttribute(jpaAttribute).get(0)); - break; - case MDDCConstants.NAME_ID_FORMAT_PRECEDENCE: - relyingPartyOverridesRepresentation.setNameIdFormats(getStringListValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.DEFAULT_AUTHENTICATION_METHODS: - relyingPartyOverridesRepresentation.setAuthenticationMethods(getStringListValueOfAttribute(jpaAttribute)); - break; - case MDDCConstants.FORCE_AUTHN: - relyingPartyOverridesRepresentation.setForceAuthn(getBooleanValueOfAttribute(jpaAttribute)); - break; - default: - break; + + Optional override = getOverrideByAttributeName(jpaAttribute.getName()); + if (override.isPresent()) { + relyingPartyOverrides.put(((RelyingPartyOverrideProperty)override.get()).getName(), + getOverrideFromAttribute(jpaAttribute)); } } - return relyingPartyOverridesRepresentation; + return relyingPartyOverrides; + } + + public static Object getOverrideFromAttribute(Attribute attribute) { + RelyingPartyOverrideProperty relyingPartyOverrideProperty = customPropertiesConfiguration.getOverrides().stream() + .filter(it -> it.getAttributeFriendlyName().equals(attribute.getFriendlyName())).findFirst().get(); + + List attributeValues = attribute.getAttributeValues(); + switch(AttributeTypes.valueOf(relyingPartyOverrideProperty.getDisplayType().toUpperCase())) { + case BOOLEAN: + if (relyingPartyOverrideProperty.getPersistType() != null + && (!relyingPartyOverrideProperty.getPersistType().equalsIgnoreCase("boolean"))) { + return true; + } else { + return Boolean.valueOf(((XSBoolean) attributeValues.get(0)).getStoredValue()); + } + case INTEGER: + return ((XSInteger) attributeValues.get(0)).getValue(); + case STRING: + if (attributeValues.get(0) instanceof XSAny) { + return ((XSAny) attributeValues.get(0)).getTextContent(); + } else { + return ((XSString) attributeValues.get(0)).getValue(); + } + case LIST: + case SET: + return attributeValues.stream().map(it -> ((XSString) it).getValue()).collect(Collectors.toList()); + default: + throw new UnsupportedOperationException("An unsupported persist type was specified (" + relyingPartyOverrideProperty.getPersistType() + ")!"); + } } public static List getAttributeListFromAttributeReleaseList(List attributeReleaseList) { @@ -122,50 +139,76 @@ public static List getAttributeListFromA } public static List getAttributeListFromRelyingPartyOverridesRepresentation - (RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + (Map relyingPartyOverridesRepresentation) { + List overridePropertyList = customPropertiesConfiguration.getOverrides(); List list = new ArrayList<>(); if (relyingPartyOverridesRepresentation != null) { - if (relyingPartyOverridesRepresentation.isSignAssertion()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); - } - if (relyingPartyOverridesRepresentation.isDontSignResponse()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); - } - if (relyingPartyOverridesRepresentation.isTurnOffEncryption()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); - } - if (relyingPartyOverridesRepresentation.isUseSha()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants - .SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); - } - if (relyingPartyOverridesRepresentation.isIgnoreAuthenticationMethod()) { - // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate - // TODO: fix - list.add(ATTRIBUTE_UTILITY.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, - "0x1")); - } - if (relyingPartyOverridesRepresentation.isOmitNotBefore()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants - .INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); - } - if (relyingPartyOverridesRepresentation.getResponderId() != null && !"".equals(relyingPartyOverridesRepresentation.getResponderId())) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, - relyingPartyOverridesRepresentation.getResponderId())); - } - if (relyingPartyOverridesRepresentation.getNameIdFormats() != null && relyingPartyOverridesRepresentation.getNameIdFormats().size() > 0) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants - .NAME_ID_FORMAT_PRECEDENCE_FN, relyingPartyOverridesRepresentation.getNameIdFormats())); - } - if (relyingPartyOverridesRepresentation.getAuthenticationMethods() != null && relyingPartyOverridesRepresentation.getAuthenticationMethods().size() > 0) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants - .DEFAULT_AUTHENTICATION_METHODS_FN, relyingPartyOverridesRepresentation.getAuthenticationMethods())); - } - if (relyingPartyOverridesRepresentation.isForceAuthn()) { - list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(MDDCConstants.FORCE_AUTHN, MDDCConstants.FORCE_AUTHN_FN, true)); + for (Map.Entry entry : relyingPartyOverridesRepresentation.entrySet()) { + String key = (String) entry.getKey(); + RelyingPartyOverrideProperty overrideProperty = overridePropertyList.stream().filter(op -> op.getName().equals(key)).findFirst().get(); + switch (AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { + case BOOLEAN: + if (overrideProperty.getPersistType() != null && + !overrideProperty.getPersistType().equals(overrideProperty.getDisplayType())) { + list.add(ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + overrideProperty.getPersistValue())); + } else { + list.add(ATTRIBUTE_UTILITY.createAttributeWithBooleanValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (Boolean) entry.getValue())); + } + break; + case INTEGER: + list.add(ATTRIBUTE_UTILITY.createAttributeWithIntegerValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (Integer) entry.getValue())); + break; + case STRING: + list.add(ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (String) entry.getValue())); + break; + case SET: + Set setValues; + if (entry.getValue() instanceof Set) { + setValues = (Set) entry.getValue(); + } else if (entry.getValue() instanceof List) { + setValues = new HashSet<>(); + setValues.addAll((List) entry.getValue()); + } else { + throw new UnsupportedOperationException("The collection passed from the UI is neither a Set or List. This shouldn't happen. Fix this!"); + } + if (setValues.size() > 0) { + list.add(ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + new ArrayList<>(setValues))); + } + break; + case LIST: + List listValues = (List) entry.getValue(); + if (listValues.size() > 0) { + list.add(ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + listValues)); + } + break; + default: + throw new UnsupportedOperationException("getAttributeListFromRelyingPartyOverridesRepresentation was called with an unsupported type (" + overrideProperty.getDisplayType() + ")!"); + } } } return (List) (List) list; } + + + public enum AttributeTypes { + BOOLEAN, + INTEGER, + STRING, + SET, + LIST + } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index cc5a34059..9907c0ff9 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -42,6 +42,7 @@ spring.jpa.properties.hibernate.format_sql=false spring.jpa.hibernate.use-new-id-generator-mappings=true +# Set the following property to periodically write out the generated metadata files. There is no default value; the following is just an example # shibui.metadata-dir=/opt/shibboleth-idp/metadata/generated shibui.logout-url=/dashboard @@ -50,6 +51,7 @@ shibui.logout-url=/dashboard #shibui.default-password= shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema.json +shibui.entity-attributes-filters-ui-schema-location=classpath:entity-attributes-filters-ui-schema.json #Actuator endpoints (info) # Un-comment to get full git details exposed like author, abbreviated SHA-1, commit message @@ -58,5 +60,6 @@ shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema. ### # metadata-providers.xml write configuration +# Set the following property to periodically write out metadata providers configuration. There is no default value; the following is just an example # shibui.metadataProviders.target=file:/opt/shibboleth-idp/conf/shibui-metadata-providers.xml # shibui.metadataProviders.taskRunRate=30000 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 563d01073..12bfd55cc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -26,3 +26,105 @@ custom: - name: employeeNumber displayName: label.attribute-employeeNumber # Custom attributes + +# The following contains a map of "relying party overrides". +# The structure of an entry is as follows: +# - name: The name of the entry. used to uniquely identify this entry. +# displayName: This will normally be the label used when displaying this override in the UI +# displayType: The type to use when displaying this option +# helpText: This is the help-icon hover-over text +# defaultValues: One or more values to be displayed as default options in the UI +# persistType: Optional. If it is necessary to persist something different than the override's display type, +# set that type here. For example, display a boolean, but persist a string. +# persistValue: Required only when persistType is used. Defines the value to be persisted. +# attributeName: This is the name of the attribute to be used in the xml. This is assumed to be a URI. +# attributeFriendlyName: This is the friendly name associated with the above attributeName. +# +# It is imperative when defining these that the "displayType" and "persistType" are known types. +# Typos or unsupported values here will result in that override being skipped! +# Supported types are as follows: boolean, integer, string, set, list +# Note that "persistType" doesn't have to match "displayType". However, the only unmatching combination currently +# supported is a "displayType" of "boolean" and "persistType" of "string". + overrides: + # Default overrides + - name: signAssertion + displayName: label.sign-the-assertion + displayType: boolean + defaultValue: false + helpText: tooltip.sign-assertion + attributeName: http://shibboleth.net/ns/profiles/saml2/sso/browser/signAssertions + attributeFriendlyName: signAssertions + - name: dontSignResponse + displayName: label.dont-sign-the-response + displayType: boolean + defaultValue: false + helpText: tooltip.dont-sign-response + attributeName: http://shibboleth.net/ns/profiles/saml2/sso/browser/signResponses + attributeFriendlyName: signResponses + - name: turnOffEncryption + displayName: label.turn-off-encryption-of-response + displayType: boolean + defaultValue: false + helpText: tooltip.turn-off-encryption + attributeName: http://shibboleth.net/ns/profiles/encryptAssertions + attributeFriendlyName: encryptAssertions + - name: useSha + displayName: label.use-sha1-signing-algorithm + displayType: boolean + defaultValue: false + helpText: tooltip.usa-sha-algorithm + persistType: string + persistValue: shibboleth.SecurityConfiguration.SHA1 + attributeName: http://shibboleth.net/ns/profiles/securityConfiguration + attributeFriendlyName: securityConfiguration + - name: ignoreAuthenticationMethod + displayName: label.ignore-any-sp-requested-authentication-method + displayType: boolean + defaultValue: false + helpText: tooltip.ignore-auth-method + persistType: string + persistValue: 0x1 + attributeName: http://shibboleth.net/ns/profiles/disallowedFeatures + attributeFriendlyName: disallowedFeatures + - name: omitNotBefore + displayName: label.omit-not-before-condition + displayType: boolean + defaultValue: false + helpText: tooltip.omit-not-before-condition + attributeName: http://shibboleth.net/ns/profiles/includeConditionsNotBefore + attributeFriendlyName: includeConditionsNotBefore + - name: responderId + displayName: label.responder-id + displayType: string + defaultValue: null + helpText: tooltip.responder-id + attributeName: http://shibboleth.net/ns/profiles/responderId + attributeFriendlyName: responderId + - name: nameIdFormats + displayName: label.nameid-format-to-send + displayType: set + helpText: tooltip.nameid-format + defaultValues: + - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + - urn:oasis:names:tc:SAML:2.0:nameid-format:transient + attributeName: http://shibboleth.net/ns/profiles/nameIDFormatPrecedence + attributeFriendlyName: nameIDFormatPrecedence + - name: authenticationMethods + displayName: label.authentication-methods-to-use + displayType: set + helpText: tooltip.authentication-methods-to-use + defaultValues: + - https://refeds.org/profile/mfa + - urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken + - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + attributeName: http://shibboleth.net/ns/profiles/defaultAuthenticationMethods + attributeFriendlyName: defaultAuthenticationMethods + - name: forceAuthn + displayName: label.force-authn + displayType: boolean + defaultValue: false + helpText: tooltip.force-authn + attributeName: http://shibboleth.net/ns/profiles/forceAuthn + attributeFriendlyName: forceAuthn \ No newline at end of file diff --git a/backend/src/main/resources/entity-attributes-filters-ui-schema.json b/backend/src/main/resources/entity-attributes-filters-ui-schema.json new file mode 100644 index 000000000..5d71f0ba8 --- /dev/null +++ b/backend/src/main/resources/entity-attributes-filters-ui-schema.json @@ -0,0 +1,228 @@ +{ + "title": "EntityAttributes Filter", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "name": { + "title": "label.filter-name", + "description": "tooltip.filter-name", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "type": "string", + "widget": { + "id": "hidden" + }, + "default": "EntityAttributes" + }, + "resourceId": { + "type": "string", + "widget": { + "id": "hidden" + } + }, + "version": { + "type": "integer", + "widget": { + "id": "hidden" + } + }, + "filterEnabled": { + "title": "label.enable-filter", + "description": "tooltip.enable-filter", + "type": "boolean", + "default": false + }, + "entityAttributesFilterTarget": { + "title": "label.search-criteria", + "description": "tooltip.search-criteria", + "type": "object", + "widget": { + "id": "filter-target" + }, + "properties": { + "entityAttributesFilterTargetType": { + "title": "", + "type": "string", + "default": "ENTITY", + "oneOf": [ + { + "enum": [ + "ENTITY" + ], + "description": "value.entity-id" + }, + { + "enum": [ + "REGEX" + ], + "description": "value.regex" + }, + { + "enum": [ + "CONDITION_SCRIPT" + ], + "description": "value.script" + } + ] + }, + "value": { + "type": "array", + "buttons": [ + { + "id": "preview", + "label": "action.preview", + "widget": "icon-button" + } + ], + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + } + }, + "required": ["value", "entityAttributesFilterTargetType"] + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "title": "label.nameid-format", + "type": "string", + "widget": { + "id": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + } + }, + "default": null + }, + "authenticationMethods": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + } + }, + "required": [ + "name" + ], + "fieldsets": [ + { + "type": "group-lg", + "fields": [ + "name", + "@type", + "resourceId", + "version", + "entityAttributesFilterTarget" + ] + }, + { + "type": "group", + "fields": [ + "filterEnabled", + "relyingPartyOverrides" + ] + }, + { + "type": "group", + "fields": [ + "attributeRelease" + ] + } + ], + "definitions": { + } +} \ No newline at end of file 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 4e4b98f31..52c6073d5 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 @@ -217,6 +220,7 @@ label.entity-id=Entity ID label.service-provider-name=Service Provider Name label.organization=Organization label.contacts=Contacts +label.contact=Contact label.mdui=MDUI Information label.service-provider-sso-descriptor=Service Provider Sso Descriptor label.service-enabled=Service Enabled @@ -295,6 +299,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 @@ -340,6 +346,8 @@ label.max-cache-duration=Max Cache Duration label.max-idle-entity-data=Max Idle Entity Data label.remove-idle-entity-data=Remove Idle Entity Data? label.cleanup-task-interval=Cleanup Task Interval +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. @@ -437,6 +445,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. @@ -469,3 +478,4 @@ tooltip.max-cache-duration=The maximum duration for which metadata will be cache tooltip.max-idle-entity-data=The maximum duration for which metadata will be allowed to be idle (no requests for it) before it is removed from the cache. tooltip.remove-idle-entity-data=Flag indicating whether idle metadata should be removed. tooltip.cleanup-task-interval=The interval at which the internal cleanup task should run. This task performs background maintenance tasks, such as the removal of expired and idle metadata. +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/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index c06e299a6..4d6ef830c 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -300,61 +300,7 @@ }, "relyingPartyOverrides": { "type": "object", - "properties": { - "signAssertion": { - "title": "label.sign-the-assertion", - "description": "tooltip.sign-assertion", - "type": "boolean", - "default": false - }, - "dontSignResponse": { - "title": "label.dont-sign-the-response", - "description": "tooltip.dont-sign-response", - "type": "boolean", - "default": false - }, - "turnOffEncryption": { - "title": "label.turn-off-encryption-of-response", - "description": "tooltip.turn-off-encryption", - "type": "boolean", - "default": false - }, - "useSha": { - "title": "label.use-sha1-signing-algorithm", - "description": "tooltip.usa-sha-algorithm", - "type": "boolean", - "default": false - }, - "ignoreAuthenticationMethod": { - "title": "label.ignore-any-sp-requested-authentication-method", - "description": "tooltip.ignore-auth-method", - "type": "boolean", - "default": false - }, - "forceAuthn": { - "title": "label.force-authn", - "description": "tooltip.force-authn", - "type": "boolean", - "default": false - }, - "omitNotBefore": { - "title": "label.omit-not-before-condition", - "type": "boolean", - "description": "tooltip.omit-not-before-condition", - "default": false - }, - "nameIdFormats": { - "$ref": "#/definitions/NameIdFormatList" - }, - "authenticationMethods": { - "$ref": "#/definitions/AuthenticationMethodList" - }, - "responderId": { - "title": "label.responder-id", - "description": "tooltip.responder-id", - "type": "string" - } - } + "properties": {} }, "attributeRelease": { "type": "array", @@ -370,6 +316,7 @@ }, "definitions": { "Contact": { + "title": "label.contact", "type": "object", "required": [ "name", @@ -525,50 +472,6 @@ } } }, - "NameIdFormatList": { - "title": "label.nameid-format-to-send", - "placeholder": "label.nameid-format", - "description": "tooltip.nameid-format", - "type": "array", - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "widget": { - "id": "datalist", - "data": [ - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" - ] - } - }, - "default": null - }, - "AuthenticationMethodList": { - "title": "label.authentication-methods-to-use", - "description": "tooltip.authentication-methods-to-use", - "type": "array", - "placeholder": "label.authentication-method", - "uniqueItems": true, - "items": { - "type": "string", - "title": "label.authentication-method", - "minLength": 1, - "maxLength": 255, - "widget": { - "id": "datalist", - "data": [ - "https://refeds.org/profile/mfa", - "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", - "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" - ] - } - }, - "default": null - }, "LogoutEndpoint": { "title": "label.new-endpoint", "description": "tooltip.new-endpoint", 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 9b6a5df54..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 @@ -1,7 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration @@ -11,6 +11,11 @@ import org.springframework.core.io.ResourceLoader import org.springframework.test.context.ActiveProfiles 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 + /** * @author Dmitriy Kopylenko */ @@ -36,11 +41,29 @@ class BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests extends Speci @TestConfiguration static class Config { @Bean - MetadataSourcesJsonSchemaResourceLocation metadataSourcesJsonSchemaResourceLocation(ResourceLoader resourceLoader, - ObjectMapper jacksonMapper) { + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, + ObjectMapper jacksonMapper) { - new MetadataSourcesJsonSchemaResourceLocation('classpath:metadata-sources-ui-schema_MALFORMED.json', - resourceLoader, jacksonMapper, false) + JsonSchemaResourceLocationRegistry.inMemory() + .register(METADATA_SOURCES, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation('classpath:metadata-sources-ui-schema_MALFORMED.json') + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(false) + .build()) + //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/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index 33a407f1a..8bf12484a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import groovy.json.JsonOutput import net.shibboleth.ext.spring.resource.ResourceHelper import org.joda.time.DateTime import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver @@ -15,6 +16,7 @@ import org.springframework.context.annotation.Bean import org.springframework.core.io.ClassPathResource import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.web.util.DefaultUriBuilderFactory import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -59,17 +61,7 @@ class EntitiesControllerIntegrationTests extends Specification { "serviceEnabled":false, "createdDate":null, "modifiedDate":null, - "relyingPartyOverrides":{ - "signAssertion":false, - "dontSignResponse":false, - "turnOffEncryption":false, - "useSha":false, - "ignoreAuthenticationMethod":false, - "omitNotBefore":false, - "responderId":null, - "nameIdFormats":[], - "authenticationMethods":[] - }, + "relyingPartyOverrides":{}, "attributeRelease":["givenName","employeeNumber"] } ''' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy index 89a43e4a8..99a8a0fd4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy @@ -77,17 +77,7 @@ class EntitiesControllerTests extends Specification { "serviceEnabled":false, "createdDate":null, "modifiedDate":null, - "relyingPartyOverrides":{ - "signAssertion":false, - "dontSignResponse":false, - "turnOffEncryption":false, - "useSha":false, - "ignoreAuthenticationMethod":false, - "omitNotBefore":false, - "responderId":null, - "nameIdFormats":[], - "authenticationMethods":[] - }, + "relyingPartyOverrides":{}, "attributeRelease":["givenName","employeeNumber"] } ''' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index bc3de95e5..122a349ae 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -448,18 +448,7 @@ class EntityDescriptorControllerTests extends Specification { "serviceEnabled": false, "createdDate": null, "modifiedDate": null, - "relyingPartyOverrides": { - "signAssertion": false, - "dontSignResponse": false, - "turnOffEncryption": false, - "useSha": false, - "ignoreAuthenticationMethod": false, - "omitNotBefore": false, - "responderId": null, - "nameIdFormats": [], - "authenticationMethods": [], - "forceAuthn": false - }, + "relyingPartyOverrides": {}, "attributeRelease": [ "givenName", "employeeNumber" @@ -577,18 +566,7 @@ class EntityDescriptorControllerTests extends Specification { "serviceEnabled": false, "createdDate": null, "modifiedDate": null, - "relyingPartyOverrides": { - "signAssertion": false, - "dontSignResponse": false, - "turnOffEncryption": false, - "useSha": false, - "ignoreAuthenticationMethod": false, - "omitNotBefore": false, - "responderId": null, - "nameIdFormats": [], - "authenticationMethods": [], - "forceAuthn": false - }, + "relyingPartyOverrides": {}, "attributeRelease": [ "givenName", "employeeNumber" diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy index 7f7beefe7..206421f49 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService @@ -40,6 +41,9 @@ class MetadataFiltersControllerIntegrationTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + @Autowired MetadataResolverConverterService metadataResolverConverterService @@ -54,7 +58,7 @@ class MetadataFiltersControllerIntegrationTests extends Specification { static BASE_URI = '/api/MetadataResolvers' def setup() { - generator = new TestObjectGenerator(attributeUtility) + generator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) mapper.registerModule(new JavaTimeModule()) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 13b8f188a..db1f15ab9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration @@ -46,6 +47,9 @@ class MetadataFiltersControllerTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + @Autowired FilterService filterService @@ -65,7 +69,7 @@ class MetadataFiltersControllerTests extends Specification { def setup() { randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility) + testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index c4453305f..d22902e30 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver @@ -44,6 +45,9 @@ class MetadataResolversControllerIntegrationTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + ObjectMapper mapper TestObjectGenerator generator @@ -52,7 +56,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { static BASE_URI = '/api/MetadataResolvers' def setup() { - generator = new TestObjectGenerator(attributeUtility) + generator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) mapper.registerModule(new JavaTimeModule()) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy index 7841e045b..b3afd2adf 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy @@ -2,21 +2,24 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification +@SpringBootTest class PolymorphicFiltersJacksonHandlingTests extends Specification { ObjectMapper mapper AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + TestObjectGenerator testObjectGenerator def setup() { @@ -27,7 +30,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { it.init() it }) - testObjectGenerator = new TestObjectGenerator(attributeUtility) + testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "Correct polymorphic serialization of EntityRoleWhiteListFilter"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy index 79962f546..f22c875aa 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy @@ -2,19 +2,26 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification +@SpringBootTest class PolymorphicResolversJacksonHandlingTests extends Specification { ObjectMapper mapper AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + TestObjectGenerator testObjectGenerator def setup() { @@ -25,7 +32,7 @@ class PolymorphicResolversJacksonHandlingTests extends Specification { it.init() it }) - testObjectGenerator = new TestObjectGenerator(attributeUtility) + testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "Correct polymorphic serialization of LocalDynamicMetadataResolver"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy index 872182ce8..506c2b488 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy @@ -6,7 +6,6 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfigurat import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver @@ -80,10 +79,7 @@ class MetadataResolverRepositoryTests extends Specification { it.name = 'original' it.resourceId = 'new-filter-UUID' it.attributeRelease = ['attr-for-release'] - it.relyingPartyOverrides = new RelyingPartyOverridesRepresentation().with { - it.signAssertion = true - it - } + it.setRelyingPartyOverrides(['signAssertion': true]) // to make sure it.rebuildAttributes() is called it } MetadataResolver metadataResolver = metadataResolverRepository.findAll().iterator().next() @@ -113,7 +109,7 @@ class MetadataResolverRepositoryTests extends Specification { it.value == 'attr-for-release' } } - persistedFilter.relyingPartyOverrides.signAssertion + persistedFilter.relyingPartyOverrides["signAssertion"] when: entityManager.flush() @@ -123,10 +119,7 @@ class MetadataResolverRepositoryTests extends Specification { it.name = 'updated' it.resourceId = 'new-filter-UUID' it.attributeRelease = ['attr-for-release', 'attr-for-release2'] - it.relyingPartyOverrides = new RelyingPartyOverridesRepresentation().with { - it.signAssertion = false - it - } + it.relyingPartyOverrides = ['signAssertion': false] it } metadataResolver = metadataResolverRepository.findAll().iterator().next() @@ -166,7 +159,7 @@ class MetadataResolverRepositoryTests extends Specification { it.value == 'attr-for-release2' } } - !persistedFilter.relyingPartyOverrides.signAssertion + !persistedFilter.relyingPartyOverrides["signAssertion"] } def "test persisting DynamicHttpMetadataResolver "() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 003e8c167..cae0c8afc 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -3,6 +3,7 @@ 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.domain.XSString import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter @@ -12,6 +13,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import groovy.xml.XmlUtil import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired @@ -73,11 +75,11 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { it.value = ['https://sp1.example.org'] it } - def attribute = attributeUtility.createAttributeWithArbitraryValues('here', null, 'there') + def attribute = attributeUtility.createAttributeWithStringValues('here', null, 'there') attribute.nameFormat = null attribute.namespacePrefix = 'saml' attribute.attributeValues.each { val -> - val.namespacePrefix = 'saml' + ((XSString)val).namespacePrefix = 'saml' } it.attributes = [attribute] it diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 679260897..e69ff9ad5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -1,6 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean @@ -10,22 +13,31 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRe import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.json.JacksonTester +import org.springframework.context.annotation.PropertySource +import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.DefaultNodeMatcher import org.xmlunit.diff.ElementSelectors import spock.lang.Specification +@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration]) +@SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@PropertySource("classpath:application.yml") class JPAEntityDescriptorServiceImplTests extends Specification { + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + def testObjectGenerator OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { @@ -33,14 +45,15 @@ class JPAEntityDescriptorServiceImplTests extends Specification { it } - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, - new JPAEntityServiceImpl(openSamlObjects, new AttributeUtility(openSamlObjects))) + def service JacksonTester jacksonTester RandomGenerator generator def setup() { + service = new JPAEntityDescriptorServiceImpl(openSamlObjects, + new JPAEntityServiceImpl(openSamlObjects, new AttributeUtility(openSamlObjects), customPropertiesConfiguration)) JacksonTester.initFields(this, new ObjectMapper()) generator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator() @@ -467,10 +480,7 @@ class JPAEntityDescriptorServiceImplTests extends Specification { def test = openSamlObjects.marshalToXmlString(service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { it.entityId = 'http://test.example.org/test1' - it.relyingPartyOverrides = new RelyingPartyOverridesRepresentation().with { - it.forceAuthn = true; - it - } + it.relyingPartyOverrides = ['forceAuthn': true] it })) @@ -487,7 +497,7 @@ class JPAEntityDescriptorServiceImplTests extends Specification { def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) then: - assert output.relyingPartyOverrides?.forceAuthn == true + assert output.relyingPartyOverrides?.forceAuthn == representation.relyingPartyOverrides.get("forceAuthn") } def "test ACS configuration"() { @@ -633,7 +643,8 @@ class JPAEntityDescriptorServiceImplTests extends Specification { def actualOutputJson = jacksonTester.write(actualOutputRepresentation) then: - // TODO: finish + // TODO: finish - This won't ever be identical due to transformations & null value representations + // How about reading in an actual output json and comparing with that instead? // Assertions.assertThat(actualOutputJson).isEqualToJson('/json/SHIBUI-219-3.json') assert true } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy index 83a5c76f4..df888a972 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration @@ -33,17 +34,19 @@ class JPAEntityServiceImplTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + def randomGenerator def testObjectGenerator def service def setup() { - service = new JPAEntityServiceImpl(openSamlObjects) - service.attributeUtility = attributeUtility + service = new JPAEntityServiceImpl(openSamlObjects, attributeUtility, customPropertiesConfiguration) randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility) + testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "getAttributeListFromEntityRepresentation builds an appropriate attribute list"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy index 51a6f89bc..25dd61fa5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration @@ -33,9 +34,12 @@ class JPAFilterServiceImplTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + CustomPropertiesConfiguration customPropertiesConfiguration + def setup() { randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility) + testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "createFilterFromRepresentation properly creates a filter from a representation"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 316bfebbd..8bb9bc7d4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -3,6 +3,7 @@ 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.MetadataResolverConverterConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget @@ -14,6 +15,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder import net.shibboleth.ext.spring.resource.ResourceHelper @@ -42,7 +44,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedX @SpringBootTest @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -295,7 +297,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { OpenSamlObjects openSamlObjects @Bean - MetadataResolver metadataResolver() { + MetadataResolver metadataResolver(TokenPlaceholderResolvers tokenPlaceholderResolvers) { def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) def aggregate = new ResourceBackedMetadataResolver(resource){ @Override diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy index 29db1fb1d..9311fde2d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -1,6 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.util -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import groovy.xml.XmlUtil import org.apache.commons.lang.StringUtils import org.w3c.dom.Document import org.xmlunit.builder.DiffBuilder @@ -10,18 +10,18 @@ import org.xmlunit.builder.Input * @author Bill Smith (wsmith@unicon.net) */ class TestHelpers { - static int determineCountOfAttributesFromRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + static int determineCountOfAttributesFromRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { int count = 0 - count += relyingPartyOverridesRepresentation.authenticationMethods.size() != 0 ? 1 : 0 - count += relyingPartyOverridesRepresentation.dontSignResponse ? 1 : 0 - count += relyingPartyOverridesRepresentation.ignoreAuthenticationMethod ? 1 : 0 - count += relyingPartyOverridesRepresentation.nameIdFormats.size() != 0 ? 1 : 0 - count += relyingPartyOverridesRepresentation.omitNotBefore ? 1 : 0 - count += relyingPartyOverridesRepresentation.signAssertion ? 1 : 0 - count += relyingPartyOverridesRepresentation.turnOffEncryption ? 1 : 0 - count += relyingPartyOverridesRepresentation.useSha ? 1 : 0 - count += StringUtils.isNotBlank(relyingPartyOverridesRepresentation.responderId) ? 1 : 0 + relyingPartyOverridesRepresentation.entrySet().each {entry -> + if (entry.value instanceof Collection) { + count += ((Collection)entry.value).size() != 0 ? 1 : 0 + } else if (entry.value instanceof String) { + count += StringUtils.isNotBlank((String)entry.value) ? 1 : 0 + } else { + count++ + } + } return count } @@ -35,4 +35,8 @@ class TestHelpers { .build() .hasDifferences() } + + static String XmlDocumentToString(Document document) { + return XmlUtil.serialize(document.documentElement) + } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index cecaa25e6..eaf031c3e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -1,14 +1,15 @@ package edu.internet2.tier.shibboleth.admin.ui.util +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.* import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.* import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.MDDCConstants +import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions import org.opensaml.saml.saml2.metadata.Organization import java.nio.file.Files @@ -21,12 +22,21 @@ class TestObjectGenerator { AttributeUtility attributeUtility + CustomPropertiesConfiguration customPropertiesConfiguration + RandomGenerator generator = new RandomGenerator() + TestObjectGenerator() {} + TestObjectGenerator(AttributeUtility attributeUtility) { this.attributeUtility = attributeUtility } + TestObjectGenerator(AttributeUtility attributeUtility, CustomPropertiesConfiguration customPropertiesConfiguration) { + this.attributeUtility = attributeUtility + this.customPropertiesConfiguration = customPropertiesConfiguration + } + DynamicHttpMetadataResolver buildDynamicHttpMetadataResolver() { def resolver = new DynamicHttpMetadataResolver().with { it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() @@ -258,34 +268,29 @@ class TestObjectGenerator { List buildAttributesList() { List attributes = new ArrayList<>() - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")) - } - if (generator.randomBoolean()) { - // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate - // TODO: fix - attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, generator.randomId())) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, generator.randomStringList())) - } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, generator.randomStringList())) + customPropertiesConfiguration.getOverrides().each {override -> + if (generator.randomBoolean()) { + switch (ModelRepresentationConversions.AttributeTypes.valueOf(override.getDisplayType().toUpperCase())) { + case ModelRepresentationConversions.AttributeTypes.BOOLEAN: + if (override.getPersistType() != null && + override.getPersistType() != override.getDisplayType()) { + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomString(30))) + } else { + attributes.add(attributeUtility.createAttributeWithBooleanValue(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomBoolean())) + } + break + case ModelRepresentationConversions.AttributeTypes.INTEGER: + attributes.add(attributeUtility.createAttributeWithIntegerValue(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomInt(0, 999999))) + break + case ModelRepresentationConversions.AttributeTypes.STRING: + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomString(30))) + break + case ModelRepresentationConversions.AttributeTypes.SET: + case ModelRepresentationConversions.AttributeTypes.LIST: + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomStringList())) + break + } + } } if (generator.randomBoolean()) { attributes.add(attributeUtility.createAttributeWithStringValues(MDDCConstants.RELEASE_ATTRIBUTES, generator.randomStringList())) @@ -306,20 +311,37 @@ class TestObjectGenerator { return representation } - RelyingPartyOverridesRepresentation buildRelyingPartyOverridesRepresentation() { - RelyingPartyOverridesRepresentation representation = new RelyingPartyOverridesRepresentation() - - representation.setAuthenticationMethods(generator.randomStringList()) - representation.setDontSignResponse(generator.randomBoolean()) - representation.setIgnoreAuthenticationMethod(generator.randomBoolean()) - representation.setNameIdFormats(generator.randomStringList()) - representation.setOmitNotBefore(generator.randomBoolean()) - representation.setSignAssertion(generator.randomBoolean()) - representation.setTurnOffEncryption(generator.randomBoolean()) - representation.setUseSha(generator.randomBoolean()) - representation.setResponderId(generator.randomId()) + Map buildRelyingPartyOverridesRepresentation() { + def representation = [:] + + customPropertiesConfiguration.getOverrides().each { override -> + switch (ModelRepresentationConversions.AttributeTypes.valueOf(override.getDisplayType().toUpperCase())) { + case ModelRepresentationConversions.AttributeTypes.BOOLEAN: + if (override.getPersistType() != null && + override.getPersistType() != override.getDisplayType()) { + representation.put(override.getName(), generator.randomString(30)) + } else { + representation.put(override.getName(), generator.randomBoolean()) + } + break + case ModelRepresentationConversions.AttributeTypes.INTEGER: + representation.put(override.getName(), generator.randomInt(0, 999999)) + break + case ModelRepresentationConversions.AttributeTypes.STRING: + representation.put(override.getName(), generator.randomString(30)) + break + case ModelRepresentationConversions.AttributeTypes.SET: + case ModelRepresentationConversions.AttributeTypes.LIST: + def someStrings = new ArrayList() + (0..generator.randomInt(1, 5)).each { + someStrings << generator.randomString(20) + } + representation.put(override.getName(), someStrings) + break + } + } - return representation + representation } String buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType type) { diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml index 20b513e1f..269a2f3ec 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -28,7 +28,7 @@ - there + there https://sp1.example.org diff --git a/docs/DEFAULTPROPERTIES.md b/docs/DEFAULTPROPERTIES.md new file mode 100644 index 000000000..75916235a --- /dev/null +++ b/docs/DEFAULTPROPERTIES.md @@ -0,0 +1,72 @@ +# Default properties + +This is a reflection of the default `application.properties` file included in the distribution. Note that lines +beginning with `#` are commented out. + +Please refer to the Spring Boot documentation [https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html] +for more information. + +```properties +# Server Configuration +#server.port=8080 + +# Logging Configuration +#logging.config=classpath:log4j2.xml + +logging.level.org.springframework=INFO +logging.level.edu.internet2.tier.shibboleth.admin.ui=INFO + +# Database Credentials +spring.datasource.username=shibui +spring.datasource.password=shibui + +# Database Configuration H2 +spring.datasource.url=jdbc:h2:mem:shibui;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.platform=h2 +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true + + +# Database Configuration PostgreSQL +#spring.datasource.url=jdbc:postgresql://localhost:5432/shibui +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +#Maria/MySQL DB +#spring.datasource.url=jdbc:mariadb://localhost:3306/shibui +#spring.datasource.driverClassName=org.mariadb.jdbc.Driver +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect + +# Liquibase properties +spring.liquibase.enabled=false +#spring.liquibase.change-log=classpath:edu/internet2/tier/shibboleth/admin/ui/database/masterchangelog.xml + +# Hibernate properties +# for production never ever use create, create-drop. It's BEST to use validate +spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +spring.jpa.hibernate.use-new-id-generator-mappings=true + +# Set the following property to periodically write out the generated metadata files. There is no default value; the following is just an example +# shibui.metadata-dir=/opt/shibboleth-idp/metadata/generated +shibui.logout-url=/dashboard + +# spring.profiles.active=default + +#shibui.default-password= + +#Actuator endpoints (info) +# Un-comment to get full git details exposed like author, abbreviated SHA-1, commit message +#management.info.git.mode=full + +### +# metadata-providers.xml write configuration + +# Set the following property to periodically write out metadata providers configuration. There is no default value; the following is just an example +# shibui.metadataProviders.target=file:/opt/shibboleth-idp/conf/shibui-metadata-providers.xml +# shibui.metadataProviders.taskRunRate=30000 +``` \ No newline at end of file diff --git a/docs/METADATAPROVIDERS.md b/docs/METADATAPROVIDERS.md new file mode 100644 index 000000000..32d02149b --- /dev/null +++ b/docs/METADATAPROVIDERS.md @@ -0,0 +1,22 @@ +# Metadata providers + +The application can generate a `metadata-providers.xml` configuration appropriate for use in the Shibboleth IdP. +There are 2 ways to access this configuration: through a web endpoint or a file. + +1. Web endpoint + + A request can be made to the `${ui.baseUrl}/api/MetadataResolvers` to get the + current configuration + +2. File export + + A file can be periodically written to disk. Set the application property `shibui.metadataProviders.target`, + pointing to a spring file resource. Note that there is no value set by default, and nothing will be written + out by default. A file, once defined, will be written every 30 seconds by default. To change the rate, set the + `shibui.metadataProviders.taskRunRate` application property, in milliseconds. + +## Docker considerations + +If writing the file out, one should use a mount in the docker container for the destination. While a bind mount +might be easiest, if running on a Windows host, one might run into problems. This is easily avoided by using a +volume instead. Refer to [https://docs.docker.com/storage/] for more information. \ No newline at end of file diff --git a/docs/METADATASOURCES.md b/docs/METADATASOURCES.md new file mode 100644 index 000000000..2ec2563b6 --- /dev/null +++ b/docs/METADATASOURCES.md @@ -0,0 +1,21 @@ +# Metdata Sources + +Metadata sources in the UI are individual metadata artifacts describing single entities, typically +relying parties. There are 2 ways to access these artifacts. + +1. MDQ + + _To be written_ + +2. File export + + Files can be periodically written to disk. Define the application property `shibui.metadata-dir`, + and the files will be written out by default every 30 seconds. Note that there is no default value + set for this property and no file will be written by default. To change the run rate, set the + `shibui.taskRunRate` application property, in milliseconds. + +## Docker considerations + +If writing the files out, one should use a mount in the docker container for the destination. While a bind mount +might be easiest, if running on a Windows host, one might run into problems. This is easily avoided by using a +volume instead. Refer to [https://docs.docker.com/storage/] for more information. \ 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/filter/container/edit-filter.component.ts b/ui/src/app/metadata/filter/container/edit-filter.component.ts index 6e058a464..a98cbc62d 100644 --- a/ui/src/app/metadata/filter/container/edit-filter.component.ts +++ b/ui/src/app/metadata/filter/container/edit-filter.component.ts @@ -11,6 +11,7 @@ import { UpdateFilterRequest } from '../action/collection.action'; import { CancelCreateFilter, UpdateFilterChanges } from '../action/filter.action'; import { PreviewEntity } from '../../domain/action/entity.action'; import { EntityAttributesFilterEntity } from '../../domain/entity'; +import { shareReplay } from 'rxjs/operators'; @Component({ selector: 'edit-filter-page', @@ -40,7 +41,7 @@ export class EditFilterComponent { ) { this.definition = MetadataFilterTypes.EntityAttributesFilter; - this.schema$ = this.schemaService.get(this.definition.schema); + this.schema$ = this.schemaService.get(this.definition.schema).pipe(shareReplay()); this.isSaving$ = this.store.select(fromFilter.getCollectionSaving); this.model$ = this.store.select(fromFilter.getSelectedFilter); 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 542126491..7215b55c4 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.ts +++ b/ui/src/app/metadata/filter/container/new-filter.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Subject, Observable, of } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { takeUntil, shareReplay } from 'rxjs/operators'; import * as fromFilter from '../reducer'; import { MetadataFilterTypes } from '../model'; @@ -39,7 +39,7 @@ export class NewFilterComponent implements OnDestroy, OnInit { ) { this.definition = MetadataFilterTypes.EntityAttributesFilter; - this.schema$ = this.schemaService.get(this.definition.schema); + this.schema$ = this.schemaService.get(this.definition.schema).pipe(shareReplay()); this.isSaving$ = this.store.select(fromFilter.getCollectionSaving); this.model$ = of({}); } @@ -51,6 +51,7 @@ export class NewFilterComponent implements OnDestroy, OnInit { this.statusChangeEmitted$ .pipe(takeUntil(this.ngUnsubscribe)) .subscribe(valid => { + console.log(valid); this.isValid = valid.value ? valid.value.length === 0 : true; }); 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 fc358dece..0d559c094 100644 --- a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts +++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts @@ -4,7 +4,7 @@ import { MetadataFilter } from '../../domain/model'; export const EntityAttributesFilter: FormDefinition = { label: 'EntityAttributes', type: 'EntityAttributes', - schema: 'assets/schema/filter/entity-attributes.schema.json', + schema: '/api/ui/EntityAttributesFilters', getValidators(): any { const validators = {}; return validators; 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/manager/container/dashboard-resolvers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts index 4ecd91c66..a7e0dd900 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts @@ -127,7 +127,7 @@ describe('Dashboard Resolvers List Page', () => { it('should route to the wizard page', () => { spyOn(router, 'navigate'); instance.edit(draft); - expect(router.navigate).toHaveBeenCalledWith(['metadata', 'resolver', 'new'], { + expect(router.navigate).toHaveBeenCalledWith(['metadata', 'resolver', 'new', 'blank', 'org-info'], { queryParams: { id: '1' } }); }); diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index 8f8caca24..e6577886d 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -72,7 +72,7 @@ export class DashboardResolversListComponent implements OnInit { edit(entity: MetadataEntity): void { if (entity.isDraft()) { - this.router.navigate(['metadata', 'resolver', 'new'], { + this.router.navigate(['metadata', 'resolver', 'new', 'blank', 'org-info'], { queryParams: { id: entity.getId() } 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 9945c45f6..2ccf25aea 100644 --- a/ui/src/app/metadata/provider/model/index.ts +++ b/ui/src/app/metadata/provider/model/index.ts @@ -1,15 +1,23 @@ import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form'; import { FileBackedHttpMetadataProviderEditor } from './file-backed-http.provider.form'; import { LocalDynamicMetadataProviderWizard, LocalDynamicMetadataProviderEditor } from './local-dynamic.provider.form'; +import { FileSystemMetadataProviderWizard, FileSystemMetadataProviderEditor } from './file-system.provider.form'; export const MetadataProviderWizardTypes = [ FileBackedHttpMetadataProviderWizard, + FileSystemMetadataProviderWizard, LocalDynamicMetadataProviderWizard ]; export const MetadataProviderEditorTypes = [ FileBackedHttpMetadataProviderEditor, - LocalDynamicMetadataProviderEditor + LocalDynamicMetadataProviderEditor, + FileSystemMetadataProviderEditor +]; + +export const FilterableProviders = [ + FileBackedHttpMetadataProviderEditor.type, + LocalDynamicMetadataProviderEditor.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/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index 0021727b2..566291404 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { withLatestFrom, map, distinctUntilChanged, skipWhile } from 'rxjs/operators'; +import { withLatestFrom, map, distinctUntilChanged, skipWhile, filter } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import * as fromResolver from '../reducer'; @@ -60,7 +60,7 @@ export class ResolverWizardStepComponent implements OnDestroy { }, definition })), - skipWhile(({ model, definition }) => !definition || !model), + filter(({ model, definition }) => (definition && model)), map(({ model, definition }) => definition.formatter(model)) ); @@ -76,7 +76,7 @@ export class ResolverWizardStepComponent implements OnDestroy { this.valueChangeEmitted$.pipe( withLatestFrom(this.definition$), - skipWhile(([ changes, definition ]) => !definition || !changes), + filter(([ changes, definition ]) => (!!definition && !!changes)), map(([ changes, definition ]) => definition.parser(changes.value)), withLatestFrom(this.store.select(fromResolver.getSelectedDraft)), map(([changes, original]) => ({ ...original, ...changes })) diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts index 2851feb55..fdd16fd5f 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts @@ -16,7 +16,7 @@ import { METADATA_SOURCE_WIZARD } from '../wizard-definition'; import { MetadataSourceWizard } from '../../domain/model/wizards/metadata-source-wizard'; import { initialState } from '../reducer/entity.reducer'; import { MockWizardModule } from '../../../../testing/wizard.stub'; -import { RouterStateSnapshot } from '@angular/router'; +import { RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { NgbModalStub } from '../../../../testing/modal.stub'; import { of } from 'rxjs'; import { MetadataResolver } from '../../domain/model'; @@ -115,9 +115,12 @@ describe('Resolver Wizard Component', () => { })); describe('canDeactivate method', () => { - it('should return true if moving to another edit page', async(() => { + it('should return true if moving to another page', async(() => { app.canDeactivate(null, null, { - url: 'wizard' + url: 'blank', + root: { + queryParams: { id: 'foo' } + } } as RouterStateSnapshot).subscribe((can) => { expect(can).toBe(true); }); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index ba2221fda..d6cac6053 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -26,9 +26,8 @@ import { SetDefinition, SetIndex, SetDisabled, ClearWizard } from '../../../wiza import * as fromWizard from '../../../wizard/reducer'; import { LoadSchemaRequest } from '../../../wizard/action/wizard.action'; import { UnsavedEntityComponent } from '../../domain/component/unsaved-entity.dialog'; -import { ModalService } from '../../../core/service/modal.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { UpdateChanges, Clear } from '../action/entity.action'; +import { Clear } from '../action/entity.action'; @Component({ selector: 'resolver-wizard-page', @@ -172,7 +171,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ): Observable { - if (nextState.url.match('blank')) { return of(true); } + if (nextState.url.match('blank') && !!nextState.root.queryParams.id) { return of(true); } if (Object.keys(this.changes).length > 0) { let modal = this.modalService.open(UnsavedEntityComponent); modal.componentInstance.message = 'resolver'; diff --git a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts index 024c923eb..f6e203381 100644 --- a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts @@ -120,7 +120,7 @@ export class DraftCollectionEffects { ) ), tap(() => { - this.store.dispatch(new ClearWizard()); + // this.store.dispatch(new ClearWizard()); this.store.dispatch(new Clear()); }) ); 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 }} - + + + + + +