diff --git a/.gitignore b/.gitignore index 8dd321428..3d9ba1c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -382,3 +382,6 @@ gradle-app.setting # Do not ignore typescript config !tsconfig.json + +# pac4j +pac4j-module/out/ diff --git a/README.md b/README.md index 07ce59ab0..40788dfe3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The easiest way to do this in a servlet container is through the use of system p ## Authentication -Currently, the application is wired with very simple authentication. A password for the user `user` +Currently, the application is wired with very simple authentication. A password for the user `root` can be set with the `shibui.default-password` property. If none is set, a default password will be generated and logged: diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy new file mode 100644 index 000000000..e9e530d64 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -0,0 +1,102 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration + +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.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +import javax.annotation.PostConstruct + +@Component +@Profile('dev') +class DevConfig { + private final UserRepository adminUserRepository + + private final MetadataResolverRepository metadataResolverRepository + + DevConfig(UserRepository adminUserRepository, MetadataResolverRepository metadataResolverRepository) { + this.adminUserRepository = adminUserRepository + this.metadataResolverRepository = metadataResolverRepository + } + + @Transactional + @PostConstruct + void createDevAdminUsers() { + if (adminUserRepository.count() == 0) { + def user = new User().with { + username = 'admin' + password = '{noop}adminpass' + roles.add(new Role(name: 'ROLE_ADMIN')) + it + } + + adminUserRepository.save(user) + } + } + + @Transactional + @Profile('fbhmr') + @Bean + MetadataResolver fbhmr(ModelRepresentationConversions modelRepresentationConversions) { + return this.metadataResolverRepository.save(new FileBackedHttpMetadataResolver().with { + enabled = true + xmlId = 'test-fbhmr' + name = 'test-fbhmr' + metadataURL = 'http://md.incommon.org/InCommon/InCommon-metadata.xml' + backingFile = '%{idp.home}/test-fbhmr.xml' + reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes() + httpMetadataResolverAttributes = new HttpMetadataResolverAttributes() + it.metadataFilters.add(new EntityAttributesFilter().with { + it.name = 'test' + it.filterEnabled = true + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.value = ["https://carmenwiki.osu.edu/shibboleth"] + return it + } + it.attributeRelease = ['eduPersonPrincipalName', 'givenName', 'surname', 'mail'] + it.relyingPartyOverrides = null + return it + }) + return it + }) + } + + @Profile('dhmr') + @Transactional + @Bean + MetadataResolver dhmr(ModelRepresentationConversions modelRepresentationConversions) { + return this.metadataResolverRepository.save(new DynamicHttpMetadataResolver().with { + it.enabled = true + it.xmlId = 'test-dhmr' + it.name = 'test-dhmr' + it.metadataRequestURLConstructionScheme = new MetadataQueryProtocolScheme(content: 'http://mdq-beta.incommon.org/global') + it.metadataFilters.add(new EntityAttributesFilter().with { + it.name = 'test' + it.filterEnabled = true + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.value = ["https://issues.shibboleth.net/shibboleth"] + return it + } + it.attributeRelease = ['eduPersonPrincipalName', 'givenName', 'surname', 'mail'] + it.relyingPartyOverrides = null + return it + }) + return it + }) + } +} 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 index 4da9b949f..e6c878b0b 100644 --- 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 @@ -11,8 +11,8 @@ 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 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 @@ -41,12 +41,12 @@ class MetadataResolverUiDefinitionController { case SchemaType.FILESYSTEM_METADATA_RESOLVER: jsonSchemaLocation = filesystemMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) break -/* case SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER: + case SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER: jsonSchemaLocation = localDynamicMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) - break*/ -/* case SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER: + break + case SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER: jsonSchemaLocation = dynamicHttpMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) - break*/ + break default: throw new UnsupportedOperationException("Json schema for an unsupported metadata resolver (" + resolverType + ") was requested") } 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 index 712142172..934d7b1a7 100644 --- 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 @@ -38,15 +38,21 @@ class RelyingPartyOverridesJsonSchemaValidatingControllerAdvice extends RequestB } @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]) + HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) + throws IOException { + def bytes = inputMessage.body.bytes def schema = Json.schema(this.jsonSchemaLocation.uri) - def validationResult = schema.validate(relyingPartyOverridesJson) + + def stream = new ByteArrayInputStream(bytes) + def validationResult = schema.validate(Json.read(stream.getText())) if (!validationResult.at('ok')) { throw new JsonSchemaValidationFailedException(validationResult.at('errors').asList()) } - body + return [ + getBody: { new ByteArrayInputStream(bytes) }, + getHeaders: { inputMessage.headers } + ] as HttpInputMessage } @ExceptionHandler(JsonSchemaValidationFailedException) 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 4a95484e8..c444bd214 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 @@ -10,7 +10,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetada import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataRequestURLConstructionScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects @@ -287,6 +291,36 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { httpMaxCacheEntries: resolver.httpMetadataResolverAttributes?.httpMaxCacheEntries, httpMaxCacheEntrySize: resolver.httpMetadataResolverAttributes?.httpMaxCacheEntrySize) { + switch (MetadataRequestURLConstructionScheme.SchemeType.get(resolver.metadataRequestURLConstructionScheme.type)) { + case MetadataRequestURLConstructionScheme.SchemeType.METADATA_QUERY_PROTOCOL: + MetadataQueryProtocolScheme scheme = (MetadataQueryProtocolScheme) resolver.metadataRequestURLConstructionScheme + MetadataQueryProtocol(transformRef: scheme.transformRef) { + if (scheme.content != null) { + mkp.yield(scheme.content) + } + } + break + case MetadataRequestURLConstructionScheme.SchemeType.TEMPLATE: + TemplateScheme scheme = (TemplateScheme) resolver.metadataRequestURLConstructionScheme + Template(encodingStyle: scheme.encodingStyle, + transformRef: scheme.transformRef, + velocityEngine: scheme.velocityEngine) { + if (scheme.content != null) { + mkp.yield(scheme.content) + } + } + break + case MetadataRequestURLConstructionScheme.SchemeType.REGEX: + RegexScheme scheme = (RegexScheme) resolver.metadataRequestURLConstructionScheme + Regex(match: scheme.match) { + if (scheme.content != null) { + mkp.yield(scheme.content) + } + } + break + default: + break + } childNodes() } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java index a1ccd6c25..2bd90bcc3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -1,16 +1,17 @@ package edu.internet2.tier.shibboleth.admin.ui; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; @@ -18,15 +19,18 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @SpringBootApplication @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "edu.internet2.tier.shibboleth.admin.ui.configuration.auto.*")) -@EntityScan(basePackages = "edu.internet2.tier.shibboleth.admin.ui.domain") +@EntityScan(basePackages = {"edu.internet2.tier.shibboleth.admin.ui.domain", "edu.internet2.tier.shibboleth.admin.ui.security.model"}) @EnableJpaAuditing @EnableScheduling @EnableWebSecurity public class ShibbolethUiApplication extends SpringBootServletInitializer { + private static final Logger logger = LoggerFactory.getLogger(ShibbolethUiApplication.class); + @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(ShibbolethUiApplication.class); @@ -44,10 +48,29 @@ public static class MetadataResolversResourceIdEmitter { MetadataResolverRepository metadataResolverRepository; @EventListener - void showMetadataResolversResourceIds(ApplicationStartedEvent e) { + public void showMetadataResolversResourceIds(ApplicationStartedEvent e) { metadataResolverRepository.findAll() - .forEach(it -> System.out.println(String.format("MetadataResolver [%s: %s]", it.getName(), it.getResourceId()))); + .forEach(it -> logger.info(String.format("MetadataResolver [%s: %s]", it.getName(), it.getResourceId()))); } } + @Component + public static class MetadataResolverInitializingApplicationStartupListener { + + @Autowired + MetadataResolverService metadataResolverService; + + @Autowired + MetadataResolverRepository metadataResolverRepository; + + @Transactional + @EventListener + public void initializeResolvers(ApplicationStartedEvent e) { + metadataResolverRepository.findAll() + .forEach(it -> { + logger.info(String.format("Reloading filters for resolver [%s: %s]", it.getName(), it.getResourceId())); + metadataResolverService.reloadFilters(it.getResourceId()); + }); + } + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index 4c4ad86db..f179c7880 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -13,8 +13,8 @@ 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; +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 @@ -38,15 +38,15 @@ public class JsonSchemaComponentsConfiguration { @Setter private String filesystemMetadataResolverUiSchemaLocation = "classpath:file-system-metadata-provider.schema.json"; -/* TODO: Will be added as part of SHIBUI-703 + //Configured via @ConfigurationProperties (using setter method) with 'shibui.local-dynamic-metadata-provider-ui-schema-location' property and + // default value set here if that property is not explicitly set in application.properties @Setter private String localDynamicMetadataResolverUiSchemaLocation = "classpath:local-dynamic-metadata-provider.schema.json"; -*/ -/* TODO: Will be added as part of SHIBUI-704 + //Configured via @ConfigurationProperties (using setter method) with 'shibui.dynamic-http-metadata-provider-ui-schema-location' property and + // default value set here if that property is not explicitly set in application.properties @Setter private String dynamicHttpMetadataResolverUiSchemaLocation = "classpath:dynamic-http-metadata-provider.schema.json"; -*/ @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { @@ -68,20 +68,19 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(true) - .build()); - /*.register(DYNAMIC_HTTP_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() - .jsonSchemaLocation(dynamicHttpMetadataResolverUiSchemaLocation) + .build()) + .register(LOCAL_DYNAMIC_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(localDynamicMetadataResolverUiSchemaLocation) .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(true) .build()) - .register(LOCAL_DYNAMIC_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() - .jsonSchemaLocation(localDynamicMetadataResolverUiSchemaLocation) + .register(DYNAMIC_HTTP_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(dynamicHttpMetadataResolverUiSchemaLocation) .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(true) - .build());*/ - + .build()); } @Bean diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java index ad407cfb5..6378990a5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java @@ -5,7 +5,6 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; -import edu.internet2.tier.shibboleth.admin.ui.service.TokenPlaceholderValueResolvingService; import edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.ResolverException; @@ -16,10 +15,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.Set; /** * this is a temporary class until a better way of doing this is found. @@ -41,9 +43,10 @@ public class MetadataResolverConfiguration { MetadataResolverConverterService metadataResolverConverterService; @Bean + @Transactional //This injected dependency makes sure that this bean has been created and the wrapped placeholder resolver service //is available via static facade accessor method to all the downstream non-Spring managed consumers - public MetadataResolver metadataResolver(TokenPlaceholderResolvers tokenPlaceholderResolvers) throws ResolverException, ComponentInitializationException { + public MetadataResolver metadataResolver(TokenPlaceholderResolvers tokenPlaceholderResolvers, Optional> metadataResolvers) throws ResolverException, ComponentInitializationException { ChainingMetadataResolver metadataResolver = new OpenSamlChainingMetadataResolver(); metadataResolver.setId("chain"); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index f824ca8a5..1dcdc6ce7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -1,6 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration.auto; import edu.internet2.tier.shibboleth.admin.ui.security.DefaultAuditorAware; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -13,6 +16,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.StrictHttpFirewall; @@ -20,7 +25,7 @@ /** * Web security configuration. - * + *

* Workaround for slashes in URL from [https://stackoverflow.com/questions/48453980/spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url] */ @Configuration @@ -34,6 +39,9 @@ public class WebSecurityConfig { @Value("${shibui.default-password:}") private String defaultPassword; + @Autowired + private UserRepository userRepository; + @Bean public HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); @@ -62,13 +70,15 @@ protected void configure(HttpSecurity http) throws Exception { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO: more configurable authentication + PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); if (defaultPassword != null && !"".equals(defaultPassword)) { auth .inMemoryAuthentication() - .withUser("user").password(defaultPassword).roles("USER"); - } else { - super.configure(auth); + .withUser("root") + .password(defaultPassword) + .roles("ADMIN"); } + auth.userDetailsService(adminUserService(userRepository)).passwordEncoder(passwordEncoder); } @Override @@ -85,6 +95,12 @@ public AuditorAware defaultAuditorAware() { return new DefaultAuditorAware(); } + @Bean + @Profile("!no-auth") + public AdminUserService adminUserService(UserRepository userRepository) { + return new AdminUserService(userRepository); + } + @Bean @Profile("no-auth") public WebSecurityConfigurerAdapter noAuthUsedForEaseDevelopment() { @@ -103,3 +119,4 @@ public void configure(WebSecurity web) throws Exception { }; } } + diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java index 2250a0b18..63c1ffa36 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java @@ -219,9 +219,11 @@ public void setNil(@Nullable XSBooleanValue xsBooleanValue) { } + private transient final LockableClassToInstanceMultiMap objectMetadata = new LockableClassToInstanceMultiMap<>(true); + @Nonnull public LockableClassToInstanceMultiMap getObjectMetadata() { - return null; + return objectMetadata; } public String getSchemaTypeNamespaceURI() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java index d1b3692d2..b98d4188b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java @@ -5,9 +5,11 @@ import lombok.Setter; import lombok.ToString; +import javax.persistence.CascadeType; import javax.persistence.ElementCollection; import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.OneToOne; import javax.persistence.OrderColumn; import java.util.List; @@ -36,6 +38,9 @@ public class DynamicHttpMetadataResolver extends MetadataResolver { @OrderColumn private List supportedContentTypes; + @OneToOne(cascade = CascadeType.ALL) + private MetadataRequestURLConstructionScheme metadataRequestURLConstructionScheme; + public DynamicHttpMetadataResolver() { type = "DynamicHttpMetadataResolver"; this.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java index 20ca36251..6b722803a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java @@ -16,6 +16,7 @@ public class LocalDynamicMetadataResolver extends MetadataResolver { public LocalDynamicMetadataResolver() { type = "LocalDynamicMetadataResolver"; + setDoInitialization(false); } private String sourceDirectory; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java new file mode 100644 index 000000000..cc6dffb98 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Entity +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +public class MetadataQueryProtocolScheme extends MetadataRequestURLConstructionScheme { + + public MetadataQueryProtocolScheme() { + type = "MetadataQueryProtocol"; + } + + private String transformRef; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java new file mode 100644 index 000000000..adddcc5aa --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java @@ -0,0 +1,61 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; +import javax.persistence.Transient; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Entity +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "@type", visible = true) +@JsonSubTypes({@JsonSubTypes.Type(value=MetadataQueryProtocolScheme.class, name="MetadataQueryProtocol"), + @JsonSubTypes.Type(value=TemplateScheme.class, name="Template"), + @JsonSubTypes.Type(value=RegexScheme.class, name="Regex")}) +public abstract class MetadataRequestURLConstructionScheme extends AbstractAuditable { + public enum SchemeType { + METADATA_QUERY_PROTOCOL("MetadataQueryProtocol"), + TEMPLATE("Template"), + REGEX("Regex"); + + private String schemeType; + private static final Map lookup = new HashMap<>(); + + static { + for (SchemeType schemeType : SchemeType.values()) { + lookup.put(schemeType.toString(), schemeType); + } + } + + SchemeType(String schemeType) { + this.schemeType = schemeType; + } + + public static SchemeType get(String schemeType) { + return lookup.get(schemeType); + } + + @Override + public String toString() { + return schemeType; + } + } + + @JsonProperty("@type") + @Transient + String type; + + String content; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java new file mode 100644 index 000000000..bb175432c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; +import javax.validation.constraints.NotNull; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Entity +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +public class RegexScheme extends MetadataRequestURLConstructionScheme { + + public RegexScheme() { + type = "Regex"; + } + + @NotNull + private String match; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java new file mode 100644 index 000000000..6cb0c8d90 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Entity +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +public class TemplateScheme extends MetadataRequestURLConstructionScheme { + + public TemplateScheme () { + type = "Template"; + } + + public enum EncodingStyle { + NONE, FORM, PATH, FRAGMENT + } + + private EncodingStyle encodingStyle = EncodingStyle.FORM; + + private String transformRef; + + private String velocityEngine = "shibboleth.VelocityEngine"; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFunctionDrivenDynamicHTTPMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFunctionDrivenDynamicHTTPMetadataResolver.java index 1235660d8..cd952a777 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFunctionDrivenDynamicHTTPMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFunctionDrivenDynamicHTTPMetadataResolver.java @@ -1,11 +1,19 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataRequestURLConstructionScheme; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.xml.ParserPool; import org.apache.http.impl.client.HttpClients; import org.apache.lucene.index.IndexWriter; +import org.apache.velocity.app.VelocityEngine; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; import org.opensaml.saml.metadata.resolver.impl.FunctionDrivenDynamicHTTPMetadataResolver; +import org.opensaml.saml.metadata.resolver.impl.MetadataQueryProtocolRequestURLBuilder; +import org.opensaml.saml.metadata.resolver.impl.RegexRequestURLBuilder; +import org.opensaml.saml.metadata.resolver.impl.TemplateRequestURLBuilder; /** * @author Bill Smith (wsmith@unicon.net) @@ -33,19 +41,44 @@ public OpenSamlFunctionDrivenDynamicHTTPMetadataResolver(ParserPool parserPool, this.setSupportedContentTypes(sourceResolver.getSupportedContentTypes()); + this.setMetadataFilter(new MetadataFilterChain()); + //TODO: These don't seem to be used anywhere. // In the parser, if not null, a warning is logged .. but nothing else happens with them. // sourceResolver.getMaxConnectionsPerRoute(); // sourceResolver.getMaxConnectionsTotal(); + + switch (MetadataRequestURLConstructionScheme.SchemeType.get(sourceResolver.getMetadataRequestURLConstructionScheme().getType())) { + case METADATA_QUERY_PROTOCOL: + this.setRequestURLBuilder(new MetadataQueryProtocolRequestURLBuilder(sourceResolver.getMetadataRequestURLConstructionScheme().getContent())); + break; + case TEMPLATE: + //TODO: address this later with a prebuilt configuration + TemplateScheme templateScheme = (TemplateScheme) sourceResolver.getMetadataRequestURLConstructionScheme(); + this.setRequestURLBuilder(new TemplateRequestURLBuilder( + new VelocityEngine(), //TODO: we may want to do something with this here: templateScheme.getVelocityEngine() + templateScheme.getContent(), + TemplateRequestURLBuilder.EncodingStyle.valueOf(templateScheme.getEncodingStyle().toString().toLowerCase()), + null)); //TODO: this may need to be an actual Function, but all we have is a ref + break; + case REGEX: + RegexScheme regexScheme = (RegexScheme) sourceResolver.getMetadataRequestURLConstructionScheme(); + this.setRequestURLBuilder(new RegexRequestURLBuilder(regexScheme.getMatch(), regexScheme.getContent())); + break; + default: + break; + } } @Override protected void initMetadataResolver() throws ComponentInitializationException { - super.initMetadataResolver(); + if (sourceResolver.getDoInitialization()) { + super.initMetadataResolver(); - delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), - this.sourceResolver.getResourceId(), - indexWriter); + delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), + this.sourceResolver.getResourceId(), + indexWriter); + } } public void refresh() throws ComponentInitializationException { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlLocalDynamicMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlLocalDynamicMetadataResolver.java index be6adb54d..bca96b876 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlLocalDynamicMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlLocalDynamicMetadataResolver.java @@ -38,11 +38,13 @@ public OpenSamlLocalDynamicMetadataResolver(ParserPool parserPool, @Override protected void initMetadataResolver() throws ComponentInitializationException { - super.initMetadataResolver(); + if (sourceResolver.getDoInitialization()) { + super.initMetadataResolver(); - delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), - this.sourceResolver.getResourceId(), - indexWriter); + delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), + this.sourceResolver.getResourceId(), + indexWriter); + } } public void refresh() throws ComponentInitializationException { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverDelegate.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverDelegate.java index ab4cfd8aa..a30610415 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverDelegate.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverDelegate.java @@ -29,20 +29,18 @@ public Iterable resolve(@Nullable CriteriaSet criteria) { } void addIndexedDescriptorsFromBackingStore(AbstractMetadataResolver.EntityBackingStore backingStore, String resourceId, IndexWriter indexWriter) throws ComponentInitializationException { - for (String entityId : backingStore.getIndexedDescriptors().keySet()) { - Document document = new Document(); - document.add(new StringField("id", entityId, Field.Store.YES)); - document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block - document.add(new StringField("tag", resourceId, Field.Store.YES)); - try { + try { + indexWriter.deleteAll(); + for (String entityId : backingStore.getIndexedDescriptors().keySet()) { + Document document = new Document(); + document.add(new StringField("id", entityId, Field.Store.YES)); + document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block + document.add(new StringField("tag", resourceId, Field.Store.YES)); indexWriter.addDocument(document); - } catch (IOException e) { - logger.error(e.getMessage(), e); } - } - try { indexWriter.commit(); } catch (IOException e) { + logger.error(e.getMessage(), e); throw new ComponentInitializationException(e); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java index 0083450b3..12c5ae67d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java @@ -3,8 +3,8 @@ 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; +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. @@ -52,15 +52,29 @@ public static JsonSchemaResourceLocation filesystemMetadataProviderSchema(JsonSc .orElseThrow(() -> new IllegalStateException("JSON schema resource location for filesystem metadata resolver is not registered.")); } -/* public static JsonSchemaResourceLocation localDynamicMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + /** + * Searches local dynamic metadata resolver JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return local dynamic metadata resolver JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + 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) { + /** + * Searches dynamic http metadata resolver JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return dynamic http metadata resolver JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + public static JsonSchemaResourceLocation dynamicHttpMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { return resourceLocationRegistry .lookup(DYNAMIC_HTTP_METADATA_RESOLVER) .orElseThrow(() -> new IllegalStateException("JSON schema resource location for dynamic http metadata resolver is not registered.")); - }*/ + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java index 05280d45e..ad0f4cd84 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java @@ -102,9 +102,9 @@ public enum SchemaType { // resolver types FILE_BACKED_HTTP_METADATA_RESOLVER("FileBackedHttpMetadataResolver"), - FILESYSTEM_METADATA_RESOLVER("FilesystemMetadataResolver"); -// LOCAL_DYNAMIC_METADATA_RESOLVER, -// DYNAMIC_HTTP_METADATA_RESOLVER; + FILESYSTEM_METADATA_RESOLVER("FilesystemMetadataResolver"), + LOCAL_DYNAMIC_METADATA_RESOLVER("LocalDynamicMetadataResolver"), + DYNAMIC_HTTP_METADATA_RESOLVER("DynamicHttpMetadataResolver"); String jsonType; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java new file mode 100644 index 000000000..20564093d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToMany; +import java.util.HashSet; +import java.util.Set; + +/** + * Models a basic administrative role concept in the system. + * + * @author Dmitriy Kopylenko + */ +@Entity +@NoArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(callSuper = true, exclude = "users") +@ToString(exclude = "users") +public class Role extends AbstractAuditable { + + @Column(unique = true) + private String name; + + @ManyToMany(cascade = CascadeType.ALL, mappedBy = "roles", fetch = FetchType.EAGER) + private Set users = new HashSet<>(); + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java new file mode 100644 index 000000000..9b24cf946 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import lombok.*; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import java.util.HashSet; +import java.util.Set; + +/** + * Models a basic administrative user in the system. + * + * @author Dmitriy Kopylenko + */ +@Entity +@NoArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(callSuper = true, exclude = "roles") +@ToString(exclude = "roles") +public class User extends AbstractAuditable { + + @Column(nullable = false, unique = true) + private String username; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @Column(nullable = false) + private String password; + + private String firstName; + + private String lastName; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @ManyToMany(cascade = CascadeType.ALL) + @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles = new HashSet<>(); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java new file mode 100644 index 000000000..120f2938e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * Spring Data repository to manage entities of type {@link Role}. + * + * @author Dmitriy Kopylenko + */ +public interface RoleRepository extends JpaRepository { + + Optional findByName(final String name); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java new file mode 100644 index 000000000..f19ceb1b7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * Spring Data repository to manage entities of type {@link User}. + * + * @author Dmitriy Kopylenko + */ +public interface UserRepository extends JpaRepository { + + Optional findByUsername(String username); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java new file mode 100644 index 000000000..a2cab06e2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserService.java @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.springsecurity; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; +import static java.util.stream.Collectors.toSet; + +/** + * Spring Security {@link UserDetailsService} implementation for local administration of admin users in the system. + * + * @author Dmitriy Kopylenko + */ +@RequiredArgsConstructor +public class AdminUserService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(String.format("User [%s] is not found", username))); + + Set grantedAuthorities = user.getRoles().stream() + .map(Role::getName) + .map(SimpleGrantedAuthority::new) + .collect(toSet()); + + if (grantedAuthorities.isEmpty()) { + //As defined by the UserDetailsService API contract + throw new UsernameNotFoundException(String.format("No roles are defined for user [%s]", username)); + } + + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); + } +} + 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 31a8b28ae..ae443a055 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 @@ -105,14 +105,21 @@ public List getAttributeListFromRelyingPartyOverridesRepresentation(M switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { case BOOLEAN: if (overrideProperty.getPersistType() != null && - !overrideProperty.getPersistType().equalsIgnoreCase("boolean")) { + !overrideProperty.getPersistType().equalsIgnoreCase("boolean") && + ((entry.getValue() instanceof Boolean && (Boolean)entry.getValue()) || Boolean.valueOf((String)entry.getValue()))) { list.add(attributeUtility.createAttributeWithStringValues(overrideProperty.getAttributeName(), overrideProperty.getAttributeFriendlyName(), - (String) entry.getValue())); + overrideProperty.getPersistValue())); } else { - list.add(attributeUtility.createAttributeWithBooleanValue(overrideProperty.getAttributeName(), - overrideProperty.getAttributeFriendlyName(), - (Boolean) entry.getValue())); + if (entry.getValue() instanceof String) { + list.add(attributeUtility.createAttributeWithBooleanValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + Boolean.valueOf((String) entry.getValue()))); + } else { + list.add(attributeUtility.createAttributeWithBooleanValue(overrideProperty.getAttributeName(), + overrideProperty.getAttributeFriendlyName(), + (Boolean) entry.getValue())); + } } break; case INTEGER: 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 638a6307f..ccb77164c 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 @@ -79,12 +79,15 @@ private OpenSamlLocalDynamicMetadataResolver convertToOpenSamlRepresentation(Loc IndexWriter indexWriter = indexWriterService.getIndexWriter(resolver.getResourceId()); XMLObjectLoadSaveManager manager = null; - try { - manager = new FilesystemLoadSaveManager(placeholderResolverService() - .resolveValueFromPossibleTokenPlaceholder(resolver.getSourceDirectory())); - } catch (ConstraintViolationException e) { - // the base directory string instance was null or empty - //TODO: What should we do here? Currently, this causes a test to fail. + if (resolver.getDoInitialization()) { + try { + manager = new FilesystemLoadSaveManager(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(resolver.getSourceDirectory())); + } catch (ConstraintViolationException e) { + // the base directory string instance was null or empty + //TODO: What should we do here? Currently, this causes a test to fail. + throw new RuntimeException("An exception occurred while attempting to instantiate a FilesystemLoadSaveManger for the path: " + resolver.getSourceDirectory(), e); + } } OpenSamlLocalDynamicMetadataResolver openSamlResolver = new OpenSamlLocalDynamicMetadataResolver(openSamlObjects.getParserPool(), indexWriter, resolver, manager); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 9907c0ff9..9ef60951e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,6 +4,7 @@ # Logging Configuration #logging.config=classpath:log4j2.xml +#logging.level.org.springframework.security=INFO logging.level.org.springframework=INFO logging.level.edu.internet2.tier.shibboleth.admin.ui=INFO diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json new file mode 100644 index 000000000..f9dec02b8 --- /dev/null +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -0,0 +1,684 @@ +{ + "type": "object", + "required": [ + "xmlId", + "metadataURL", + "metadataRequestURLConstructionScheme" + ], + "properties": { + "enabled": { + "title": "label.enable-provider-upon-saving", + "description": "tooltip.enable-provider-upon-saving", + "type": "boolean", + "default": false + }, + "xmlId": { + "title": "label.xml-id", + "description": "tooltip.xml-id", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataRequestURLConstructionScheme": { + "type": "object", + "required": [ + "@type", + "content" + ], + "anyOf": [ + { + "properties": { + "@type": { + "enum": [ + "Regex" + ] + } + }, + "required": [ + "@type", + "content", + "match" + ] + }, + { + "properties": { + "@type": { + "enum": [ + "MetadataQueryProtocol" + ] + } + }, + "required": [ + "@type", + "content" + ] + } + ], + "properties": { + "@type": { + "title": "label.md-request-type", + "description": "tooltip.md-request-type", + "type": "string", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "MetadataQueryProtocol" + ], + "description": "value.md-query-protocol" + }, + { + "enum": [ + "Regex" + ], + "description": "value.regex" + } + ] + }, + "content": { + "title": "label.md-request-value", + "description": "tooltip.md-request-value", + "type": "string" + }, + "match": { + "title": "label.match", + "description": "tooltip.match", + "type": "string", + "visibleIf": { + "@type": [ + "Regex" + ] + } + } + } + }, + "requireValidMetadata": { + "title": "label.require-valid-metadata", + "description": "tooltip.require-valid-metadata", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "failFastInitialization": { + "title": "label.fail-fast-init", + "description": "tooltip.fail-fast-init", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "dynamicMetadataResolverAttributes": { + "type": "object", + "properties": { + "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 + }, + "minCacheDuration": { + "title": "label.min-cache-duration", + "description": "tooltip.min-cache-duration", + "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)?)?$" + }, + "maxCacheDuration": { + "title": "label.max-cache-duration", + "description": "tooltip.max-cache-duration", + "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)?)?$" + }, + "maxIdleEntityData": { + "title": "label.max-idle-entity-data", + "description": "tooltip.max-idle-entity-data", + "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)?)?$" + }, + "cleanupTaskInterval": { + "title": "label.cleanup-task-interval", + "description": "tooltip.cleanup-task-interval", + "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)?)?$" + }, + "persistentCacheManagerDirectory": { + "title": "label.persistent-cache-manager-directory", + "description": "tooltip.persistent-cache-manager-directory", + "type": "string", + "default": "", + "minLength": 1 + }, + "initializeFromPersistentCacheInBackground": { + "title": "label.initialize-from-persistent-cache-in-background", + "description": "tooltip.initialize-from-persistent-cache-in-background", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "backgroundInitializationFromCacheDelay": { + "title": "label.background-init-from-cache-delay", + "description": "tooltip.background-init-from-cache-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "visibleIf": { + "initializeFromPersistentCacheInBackground": [ + true + ] + }, + "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)?)?$" + } + } + }, + "httpMetadataResolverAttributes": { + "order": [], + "type": "object", + "fieldsets": [ + { + "title": "label.http-security-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "disregardTLSCertificate" + ] + }, + { + "title": "label.http-connection-attributes", + "type": "section", + "fields": [ + "connectionRequestTimeout", + "connectionTimeout", + "socketTimeout" + ] + }, + { + "title": "label.http-proxy-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "proxyHost", + "proxyPort", + "proxyUser", + "proxyPassword" + ] + }, + { + "title": "label.http-caching-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "httpCaching", + "httpCacheDirectory", + "httpMaxCacheEntries", + "httpMaxCacheEntrySize" + ] + }, + { + "title": "", + "type": "hidden", + "class": "col-12", + "fields": [ + "tlsTrustEngineRef", + "httpClientSecurityParametersRef", + "httpClientRef" + ] + } + ], + "properties": { + "disregardTLSCertificate": { + "type": "boolean", + "title": "label.disregard-tls-cert", + "description": "tooltip.disregard-tls-cert", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": false + }, + "httpClientRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden", + "default": "" + }, + "connectionRequestTimeout": { + "type": "string", + "title": "label.connection-request-timeout", + "description": "tooltip.connection-request-timeout", + "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)?)?$" + }, + "connectionTimeout": { + "type": "string", + "title": "label.connection-timeout", + "description": "tooltip.connection-timeout", + "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)?)?$" + }, + "socketTimeout": { + "type": "string", + "title": "label.socket-timeout", + "description": "tooltip.socket-timeout", + "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)?)?$" + }, + "tlsTrustEngineRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden", + "default": "" + }, + "httpClientSecurityParametersRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden", + "default": "" + }, + "proxyHost": { + "type": "string", + "title": "label.proxy-host", + "description": "tooltip.proxy-host", + "placeholder": "", + "default": "" + }, + "proxyPort": { + "type": "string", + "title": "label.proxy-port", + "description": "tooltip.proxy-port", + "placeholder": "", + "default": "" + }, + "proxyUser": { + "type": "string", + "title": "label.proxy-user", + "description": "tooltip.proxy-user", + "placeholder": "", + "default": "" + }, + "proxyPassword": { + "type": "string", + "title": "label.proxy-password", + "description": "tooltip.proxy-password", + "placeholder": "", + "default": "" + }, + "httpCaching": { + "type": "string", + "title": "label.http-caching", + "description": "tooltip.http-caching", + "placeholder": "label.select-caching-type", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "none" + ], + "description": "value.none" + }, + { + "enum": [ + "file" + ], + "description": "value.file" + }, + { + "enum": [ + "memory" + ], + "description": "value.memory" + } + ] + }, + "httpCacheDirectory": { + "type": "string", + "title": "label.http-caching-directory", + "description": "tooltip.http-caching-directory", + "placeholder": "" + }, + "httpMaxCacheEntries": { + "type": "integer", + "title": "label.http-max-cache-entries", + "description": "tooltip.http-max-cache-entries", + "placeholder": "", + "default": 0, + "minimum": 0 + }, + "httpMaxCacheEntrySize": { + "type": "integer", + "title": "label.max-cache-entry-size", + "description": "tooltip.max-cache-entry-size", + "placeholder": "", + "default": 0, + "minimum": 0 + } + } + }, + "metadataFilters": { + "title": "", + "description": "", + "type": "object", + "properties": { + "RequiredValidUntil": { + "title": "label.required-valid-until", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "maxValidityInterval": { + "title": "label.max-validity-interval", + "description": "tooltip.max-validity-interval", + "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)?)?$" + } + } + }, + "SignatureValidation": { + "title": "label.signature-validation-filter", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "requireSignedRoot": { + "title": "label.require-signed-root", + "description": "tooltip.require-signed-root", + "type": "boolean", + "default": true + }, + "certificateFile": { + "title": "label.certificate-file", + "description": "tooltip.certificate-file", + "type": "string", + "default": "" + } + }, + "anyOf": [ + { + "properties": { + "requireSignedRoot": { + "enum": [ + true + ] + } + }, + "required": [ + "certificateFile" + ] + }, + { + "properties": { + "requireSignedRoot": { + "enum": [ + false + ] + } + } + } + ] + }, + "EntityRoleWhiteList": { + "title": "label.entity-role-whitelist", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "retainedRoles": { + "title": "label.retained-roles", + "description": "tooltip.retained-roles", + "type": "array", + "items": { + "widget": { + "id": "select" + }, + "type": "string", + "oneOf": [ + { + "enum": [ + "SPSSODescriptor" + ], + "description": "value.spdescriptor" + }, + { + "enum": [ + "AttributeAuthorityDescriptor" + ], + "description": "value.attr-auth-descriptor" + } + ] + } + }, + "removeRolelessEntityDescriptors": { + "title": "label.remove-roleless-entity-descriptors", + "description": "tooltip.remove-roleless-entity-descriptors", + "type": "boolean", + "default": true + }, + "removeEmptyEntitiesDescriptors": { + "title": "label.remove-empty-entities-descriptors", + "description": "tooltip.remove-empty-entities-descriptors", + "type": "boolean", + "default": true + } + } + } + } + } + } +} \ 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 87aaf7299..31c4de9b8 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -76,6 +76,11 @@ value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor +value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider +value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider + +value.md-query-protocol=MetadataQueryProtocol +value.template=Template brand.header.title=Source Management brand.logo-link-label=Shibboleth @@ -218,6 +223,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 @@ -316,6 +322,7 @@ label.reloading-attributes=Reloading Attributes label.metadata-filter-plugins=Metadata Filter Plugins label.advanced-settings=Advanced Settings label.edit-metadata-provider=Edit Metadata Provider +label.http-settings-advanced=Http Settings (Advanced) label.metadata-ui=User Interface / MDUI Information label.descriptor-info=SP SSO Descriptor Information @@ -337,9 +344,27 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN +label.dynamic-attributes=Dynamic Attributes +label.min-cache-duration=Min Cache Duration +label.max-cache-duration=Max Cache Duration +label.max-idle-entity-data=Max Idle Entity Data +label.cleanup-task-interval=Cleanup Task Interval +label.persistent-cache-manager-directory=Persistent Cache Manager Directory +label.initialize-from-persistent-cache-in-background=Initialize from Persistent Cache in Background? +label.background-init-from-cache-delay=Background Initialization from Cache Delay +label.source-directory=Source Directory +label.remove-idle-entity-data=Remove Idle Entity Data? 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. +label.md-request-type=Metadata Request URL Construction Type +label.md-request-value=Metadata Request URL Construction Value +label.transform-ref=Transform Ref +label.encoding-style=Encoding Style +label.velocity-engine=Velocity Engine +label.match=Match + + 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. @@ -436,6 +461,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. @@ -462,4 +488,21 @@ tooltip.filter-name=Filter Name tooltip.enable-filter=Enable Filter? tooltip.enable-service=Enable Service? +tooltip.min-cache-duration=The minimum duration for which metadata will be cached before it is refreshed. +tooltip.max-cache-duration=The maximum duration for which metadata will be cached before it is refreshed. +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.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.persistent-cache-manager-directory=The optional manager for the persistent cache store for resolved metadata. On metadata provider initialization, data present in the persistent cache will be loaded to memory, effectively restoring the state of the provider as closely as possible to that which existed before the previous shutdown. Each individual cache entry will only be loaded if 1) the entry is still valid as determined by the internal provider logic, and 2) the entry passes the (optional) predicate supplied via initializationFromCachePredicateRef. +tooltip.initialize-from-persistent-cache-in-background=Flag indicating whether should initialize from the persistent cache in the background. Initializing from the cache in the background will improve IdP startup times. +tooltip.background-init-from-cache-delay=The delay after which to schedule the background initialization from the persistent cache when initializeFromPersistentCacheInBackground=true. + +tooltip.source-directory=Convenience mechanism for wiring a FilesystemLoadSaveManager, loading from the specified source directory in the local filesystem. This attribute will be ignored if sourceManagerRef is also specified. Either this attribute or sourceManagerRef is required. +tooltip.remove-idle-entity-data=Flag indicating whether idle metadata should be removed. + tooltip.do-resolver-initialization=Initialize this resolver? In the case of Filesystem resolvers, this will cause the system to read the file and index the resolver. +tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Template, 3) Regex. +tooltip.md-request-value=Content of the element. +tooltip.transform-ref=A reference to a transform function for the entityID. If used, the child element must be empty. +tooltip.encoding-style=Determines whether and how the entityID value will be URL encoded prior to replacement. Allowed values are: 1) "none" - no encoding is performed, 2) "form" - encoded using URL form parameter encoding (for query parameters), 3) "path" - encoded using URL path encoding, or 4) "fragment" - encoded using URL fragment encoding. The precise definition of these terms is defined in the documentation for the methods of the Guava library\u0027s UrlEscapers class. +tooltip.velocity-engine=This attribute may be used to specify the name of the Velocity engine defined within the application. +tooltip.match=A regular expression against which the entityID is evaluated. \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index e156a803d..01b5c18e2 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -76,6 +76,11 @@ value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor +value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider +value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider + +value.md-query-protocol=MetadataQueryProtocol +value.template=Template brand.header.title=Source Management brand.logo-link-label=Shibboleth @@ -317,6 +322,7 @@ label.reloading-attributes=Reloading Attributes label.metadata-filter-plugins=Metadata Filter Plugins label.advanced-settings=Advanced Settings label.edit-metadata-provider=Edit Metadata Provider +label.http-settings-advanced=Http Settings (Advanced) label.metadata-ui=User Interface / MDUI Information label.descriptor-info=SP SSO Descriptor Information @@ -338,13 +344,37 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN +label.dynamic-attributes=Dynamic Attributes +label.min-cache-duration=Min Cache Duration +label.max-cache-duration=Max Cache Duration +label.max-idle-entity-data=Max Idle Entity Data +label.cleanup-task-interval=Cleanup Task Interval +label.persistent-cache-manager-directory=Persistent Cache Manager Directory +label.initialize-from-persistent-cache-in-background=Initialize from Persistent Cache in Background? +label.background-init-from-cache-delay=Background Initialization from Cache Delay +label.source-directory=Source Directory +label.remove-idle-entity-data=Remove Idle Entity Data? 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. +label.md-request-type=Metadata Request URL Construction Type +label.md-request-value=Metadata Request URL Construction Value +label.transform-ref=Transform Ref +label.encoding-style=Encoding Style +label.velocity-engine=Velocity Engine +label.match=Match + + 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. message.id-unique=ID must be unique. +message.array-items-must-be-unique=Items in list must be unique. + +message.org-name-required=Organization Name is required. +message.org-displayName-required=Organization Name is required. +message.org-url-required=Organization Name is required. +message.org-incomplete=These three fields must all be entered if any single field has a value. message.conflict=Conflict message.data-version-contention=Data Version Contention @@ -464,4 +494,21 @@ tooltip.filter-name=Filter Name tooltip.enable-filter=Enable Filter? tooltip.enable-service=Enable Service? +tooltip.min-cache-duration=The minimum duration for which metadata will be cached before it is refreshed. +tooltip.max-cache-duration=The maximum duration for which metadata will be cached before it is refreshed. +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.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.persistent-cache-manager-directory=The optional manager for the persistent cache store for resolved metadata. On metadata provider initialization, data present in the persistent cache will be loaded to memory, effectively restoring the state of the provider as closely as possible to that which existed before the previous shutdown. Each individual cache entry will only be loaded if 1) the entry is still valid as determined by the internal provider logic, and 2) the entry passes the (optional) predicate supplied via initializationFromCachePredicateRef. +tooltip.initialize-from-persistent-cache-in-background=Flag indicating whether should initialize from the persistent cache in the background. Initializing from the cache in the background will improve IdP startup times. +tooltip.background-init-from-cache-delay=The delay after which to schedule the background initialization from the persistent cache when initializeFromPersistentCacheInBackground=true. + +tooltip.source-directory=Convenience mechanism for wiring a FilesystemLoadSaveManager, loading from the specified source directory in the local filesystem. This attribute will be ignored if sourceManagerRef is also specified. Either this attribute or sourceManagerRef is required. +tooltip.remove-idle-entity-data=Flag indicating whether idle metadata should be removed. + tooltip.do-resolver-initialization=Initialize this resolver? In the case of Filesystem resolvers, this will cause the system to read the file and index the resolver. +tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Template, 3) Regex. +tooltip.md-request-value=Content of the element. +tooltip.transform-ref=A reference to a transform function for the entityID. If used, the child element must be empty. +tooltip.encoding-style=Determines whether and how the entityID value will be URL encoded prior to replacement. Allowed values are: 1) "none" - no encoding is performed, 2) "form" - encoded using URL form parameter encoding (for query parameters), 3) "path" - encoded using URL path encoding, or 4) "fragment" - encoded using URL fragment encoding. The precise definition of these terms is defined in the documentation for the methods of the Guava library\u0027s UrlEscapers class. +tooltip.velocity-engine=This attribute may be used to specify the name of the Velocity engine defined within the application. +tooltip.match=A regular expression against which the entityID is evaluated. \ No newline at end of file diff --git a/backend/src/main/resources/local-dynamic-metadata-provider.schema.json b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json new file mode 100644 index 000000000..04aa496f9 --- /dev/null +++ b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json @@ -0,0 +1,188 @@ +{ + "type": "object", + "required": [ + "name", + "@type", + "xmlId", + "sourceDirectory" + ], + "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 + }, + "sourceDirectory": { + "title": "label.source-directory", + "description": "tooltip.source-directory", + "type": "string", + "default": "", + "minLength": 1 + }, + "enabled": { + "title": "label.enable-provider-upon-saving", + "description": "tooltip.enable-provider-upon-saving", + "type": "boolean", + "default": false + }, + "dynamicMetadataResolverAttributes": { + "type": "object", + "properties": { + "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": 0.99, + "default": null + }, + "minCacheDuration": { + "title": "label.min-cache-duration", + "description": "tooltip.min-cache-duration", + "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)?)?$" + }, + "maxCacheDuration": { + "title": "label.max-cache-duration", + "description": "tooltip.max-cache-duration", + "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)?)?$" + }, + "maxIdleEntityData": { + "title": "label.max-idle-entity-data", + "description": "tooltip.max-idle-entity-data", + "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)?)?$" + }, + "removeIdleEntityData": { + "title": "label.remove-idle-entity-data", + "description": "tooltip.remove-idle-entity-data", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "cleanupTaskInterval": { + "title": "label.cleanup-task-interval", + "description": "tooltip.cleanup-task-interval", + "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)?)?$" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index d1353043e..3f002253a 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -45,18 +45,24 @@ } }, "dependencies": { - "name": [ - "displayName", - "url" - ], - "displayName": [ - "name", - "url" - ], - "url": [ - "name", - "displayName" - ] + "name": { + "required": [ + "displayName", + "url" + ] + }, + "displayName": { + "required": [ + "name", + "url" + ] + }, + "url": { + "required": [ + "name", + "displayName" + ] + } } }, "contacts": { 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 5c4efc0b6..671961e8b 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 @@ -12,8 +12,10 @@ 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.DYNAMIC_HTTP_METADATA_RESOLVER 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.LOCAL_DYNAMIC_METADATA_RESOLVER import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES /** @@ -64,6 +66,18 @@ class BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests extends Speci .jacksonMapper(jacksonMapper) .detectMalformedJson(false) .build()) + .register(LOCAL_DYNAMIC_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation('classpath:local-dynamic-metadata-provider.schema.json') + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(false) + .build()) + .register(DYNAMIC_HTTP_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation('classpath:dynamic-http-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/security/springsecurity/AdminUserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy new file mode 100644 index 000000000..e20b5d8f8 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy @@ -0,0 +1,51 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.springsecurity + +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.test.context.ActiveProfiles +import spock.lang.Specification + +/** + * Tests for AdminUserService + * + * @author Dmitriy Kopylenko + */ +@SpringBootTest +@ActiveProfiles('dev') +class AdminUserServiceTests extends Specification { + + @Autowired + AdminUserService adminUserService + + @Autowired + RoleRepository adminRoleRepository + + @Autowired + UserRepository adminUserRepository + + + def "Loading existing admin user with admin role"() { + given: 'Valid user with admin role is available (loaded by Spring Boot Listener in dev profile)' + def user = adminUserService.loadUserByUsername('admin') + + expect: + user.username == 'admin' + user.password == '{noop}adminpass' + user.getAuthorities().size() == 1 + user.getAuthorities()[0].authority == 'ROLE_ADMIN' + user.enabled + user.accountNonExpired + user.credentialsNonExpired + } + + def "Loading NON-existing admin user with admin role"() { + when: 'Non-existent admin user is tried to be looked up' + adminUserService.loadUserByUsername('nonexisting') + + then: + thrown UsernameNotFoundException + } +} 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 cae0c8afc..08a14feb5 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 @@ -57,8 +57,6 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { metadataResolverRepository.save(mr) def output = metadataResolverService.generateConfiguration() - println(output.documentElement) - then: generatedXmlIsTheSameAsExpectedXml('/conf/278.xml', output) } @@ -76,7 +74,7 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { it } def attribute = attributeUtility.createAttributeWithStringValues('here', null, 'there') - attribute.nameFormat = null + attribute.nameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' attribute.namespacePrefix = 'saml' attribute.attributeValues.each { val -> ((XSString)val).namespacePrefix = 'saml' 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 8bb9bc7d4..1e2e61b3c 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 @@ -9,7 +9,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository @@ -280,6 +284,71 @@ class JPAMetadataResolverServiceImplTests extends Specification { generatedXmlIsTheSameAsExpectedXml('/conf/670.xml', generatedXmlDocument) } + def 'test generating DynamicHttpMetadataResolver xml snippet with MetadataQueryProtocolScheme'() { + given: + def resolver = new DynamicHttpMetadataResolver().with { + it.xmlId = 'DynamicHttpMetadataResolver' + it.metadataRequestURLConstructionScheme = new MetadataQueryProtocolScheme().with { + it.transformRef = 'This is a transformRef' + it.content = 'some content' + it + } + it + } + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/704.1.xml', domBuilder.parseText(writer.toString())) + } + + def 'test generating DynamicHttpMetadataResolver xml snippet with TemplateScheme'() { + given: + def resolver = new DynamicHttpMetadataResolver().with { + it.xmlId = 'DynamicHttpMetadataResolver' + it.metadataRequestURLConstructionScheme = new TemplateScheme().with { + it.encodingStyle = TemplateScheme.EncodingStyle.FORM + it.transformRef = 'This is a transformRef' + it.velocityEngine = 'This is a velocityEngine' + it.content = 'some content' + it + } + it + } + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/704.2.xml', domBuilder.parseText(writer.toString())) + } + + def 'test generating DynamicHttpMetadataResolver xml snippet with RegexScheme'() { + given: + def resolver = new DynamicHttpMetadataResolver().with { + it.xmlId = 'DynamicHttpMetadataResolver' + it.metadataRequestURLConstructionScheme = new RegexScheme().with { + it.match = 'This is the match field' + it.content = 'some content' + it + } + it + } + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/704.3.xml', domBuilder.parseText(writer.toString())) + } + static genXmlSnippet(MarkupBuilder xml, Closure xmlNodeGenerator) { xml.MetadataProvider('id': 'ShibbolethMetadata', 'xmlns': 'urn:mace:shibboleth:2.0:metadata', 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 eaf031c3e..071516cc2 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 @@ -512,6 +512,11 @@ class TestObjectGenerator { it.dynamicMetadataResolverAttributes = new DynamicMetadataResolverAttributes().with { it } + it.metadataRequestURLConstructionScheme = new MetadataQueryProtocolScheme().with { + it.transformRef = 'transformRef' + it.content = 'content' + it + } it } } diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml index 269a2f3ec..82a25f618 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -23,11 +23,12 @@ socketTimeout="PT5S" supportedContentTypes="[]" xsi:type="DynamicHttpMetadataProvider"> + content + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="here" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> there https://sp1.example.org diff --git a/backend/src/test/resources/conf/278.xml b/backend/src/test/resources/conf/278.xml index 3bebc347b..6db6e5cfa 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -23,6 +23,7 @@ socketTimeout="PT5S" supportedContentTypes="[]" xsi:type="DynamicHttpMetadataProvider"> + content diff --git a/backend/src/test/resources/conf/704.1.xml b/backend/src/test/resources/conf/704.1.xml new file mode 100644 index 000000000..a7e5d63a0 --- /dev/null +++ b/backend/src/test/resources/conf/704.1.xml @@ -0,0 +1,5 @@ + + + some content + + \ No newline at end of file diff --git a/backend/src/test/resources/conf/704.2.xml b/backend/src/test/resources/conf/704.2.xml new file mode 100644 index 000000000..823b89de4 --- /dev/null +++ b/backend/src/test/resources/conf/704.2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/backend/src/test/resources/conf/704.3.xml b/backend/src/test/resources/conf/704.3.xml new file mode 100644 index 000000000..d7ce637d3 --- /dev/null +++ b/backend/src/test/resources/conf/704.3.xml @@ -0,0 +1,5 @@ + + + some content + + \ No newline at end of file diff --git a/docs/GETTINGSTARTED.md b/docs/GETTINGSTARTED.md index 65da98a2c..eeed8fae3 100644 --- a/docs/GETTINGSTARTED.md +++ b/docs/GETTINGSTARTED.md @@ -46,7 +46,7 @@ The easiest way to do this in a servlet container is through the use of system p ## Authentication -Currently, the application is wired with very simple authentication. A password for the user `user` +Currently, the application is wired with very simple authentication. A password for the user `root` can be set with the `shibui.default-password` property. If none is set, a default password will be generated and logged: diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 000000000..b8397ecd9 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +Security in the system is controlled by Spring Security. + +Currently, the following roles are recognized: + +1. `ADMIN` +1. `USER` \ No newline at end of file diff --git a/run-app.sh b/run-app.sh index 1719f2b99..79e8dc2a6 100755 --- a/run-app.sh +++ b/run-app.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -./gradlew -Dshibui.logout-url=/dashboard "$@" clean bootRun npm_run_start --parallel +./gradlew -Dspring.profiles.active=no-auth -Dshibui.logout-url=/dashboard "$@" clean bootRun npm_run_start --parallel diff --git a/ui/package-lock.json b/ui/package-lock.json index f2aba6f0c..bb8441756 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -183,7 +183,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -526,7 +525,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -924,7 +922,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1475,7 +1472,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -2811,7 +2807,6 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", - "fsevents": "1.2.4", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -4641,535 +4636,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", @@ -7895,13 +7361,6 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, - "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", - "dev": true, - "optional": true - }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", @@ -11612,7 +11071,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -12460,7 +11918,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", diff --git a/ui/src/app/metadata/domain/model/providers/dynamic-http-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/dynamic-http-metadata-provider.ts new file mode 100644 index 000000000..5d3201836 --- /dev/null +++ b/ui/src/app/metadata/domain/model/providers/dynamic-http-metadata-provider.ts @@ -0,0 +1,51 @@ +import { BaseMetadataProvider } from './base-metadata-provider'; + +export interface DynamicHttpMetadataProvider extends BaseMetadataProvider { + id: string; + metadataURL: string; + dynamicMetadataResolverAttributes: DynamicMetadataResolverAttributes; + httpMetadataResolverAttributes: HttpMetadataResolverAttributes; + maxConnectionsTotal: number; + maxConnectionsPerRoute: number; + supportedContentTypes: string[]; +} + +export interface DynamicMetadataResolverAttributes { + refreshDelayFactor: number; + minCacheDuration: string; + maxCacheDuration: string; + maxIdleEntityData: string; + removeIdleEntityData: boolean; + cleanupTaskInterval: string; + + persistentCacheManagerRef: string; + persistentCacheManagerDirectory: string; + persistentCacheKeyGeneratorRef: string; + initializeFromPersistentCacheInBackground: boolean; + backgroundInitializationFromCacheDelay: string; + initializationFromCachePredicateRef: string; +} + +export interface HttpMetadataResolverAttributes { + httpClientRef; + connectionRequestTimeout: string; + connectionTimeout: string; + socketTimeout: string; + disregardTLSCertificate: boolean; + tlsTrustEngineRef: string; + httpClientSecurityParametersRef: string; + proxyHost: string; + proxyPort: string; + proxyUser: string; + proxyPassword: string; + httpCaching: HttpCachingType; + httpCacheDirectory: string; + httpMaxCacheEntries: number; + httpMaxCacheEntrySize: number; +} + +export enum HttpCachingType { + NONE = 'none', + FILE = 'file', + MEMORY = 'memory' +} diff --git a/ui/src/app/metadata/domain/model/providers/local-dynamic-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/local-dynamic-metadata-provider.ts new file mode 100644 index 000000000..b56f9237b --- /dev/null +++ b/ui/src/app/metadata/domain/model/providers/local-dynamic-metadata-provider.ts @@ -0,0 +1,7 @@ +import { BaseMetadataProvider } from './base-metadata-provider'; + +export interface LocalDynamicMetadataProvider extends BaseMetadataProvider { + id: string; + sourceDirectory: string; + reloadableMetadataResolverAttributes: any; +} diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts index 3386d65e4..0cf3344ad 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts @@ -40,7 +40,10 @@ describe('Metadata Source Base class', () => { it('should return a list of validators for the ngx-schema-form', () => { expect(Object.keys(getValidators([]))).toEqual([ '/', - '/entityId' + '/entityId', + '/organization/name', + '/organization/displayName', + '/organization/url' ]); }); }); diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index 7d29d34c5..8a6604c5a 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -60,6 +60,17 @@ export class MetadataSourceBase implements Wizard { } getValidators(entityIdList: string[]): { [key: string]: any } { + const checkOrg = (value, property, form) => { + const org = property.parent; + const orgValue = org.value || {}; + const err = Object.keys(orgValue) && !value ? { + code: 'ORG_INCOMPLETE', + path: `#${property.path}`, + message: `message.org-incomplete`, + params: [value] + } : null; + return err; + }; const validators = { '/': (value, property, form_current) => { let errors; @@ -68,12 +79,13 @@ export class MetadataSourceBase implements Wizard { const item = value[key]; const validatorKey = `/${key}`; const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; - const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + const error = validator ? validator(item, form_current.getProperty(key), form_current) : null; if (error) { errors = errors || []; errors.push(error); } }); + console.log(errors, form_current); return errors; }, '/entityId': (value, property, form) => { @@ -84,7 +96,10 @@ export class MetadataSourceBase implements Wizard { params: [value] } : null; return err; - } + }, + '/organization/name': checkOrg, + '/organization/displayName': checkOrg, + '/organization/url': checkOrg }; return validators; } diff --git a/ui/src/app/metadata/domain/service/provider.service.ts b/ui/src/app/metadata/domain/service/provider.service.ts index dca9bb1c3..f17557902 100644 --- a/ui/src/app/metadata/domain/service/provider.service.ts +++ b/ui/src/app/metadata/domain/service/provider.service.ts @@ -18,7 +18,8 @@ export class MetadataProviderService { ) {} query(): Observable { return this.http.get(`${this.base}${this.endpoint}`).pipe( - map(providers => providers.filter(p => p['@type'] !== 'BaseMetadataResolver')) + map(providers => providers.filter(p => p['@type'] !== 'BaseMetadataResolver')), + map(providers => providers.map(p => ({ ...p, id: p.resourceId }))) ); } 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 7215b55c4..aeb56e019 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.ts +++ b/ui/src/app/metadata/filter/container/new-filter.component.ts @@ -51,7 +51,6 @@ 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/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts index 4dde8f3a4..f2975bd34 100644 --- a/ui/src/app/metadata/filter/effect/collection.effect.ts +++ b/ui/src/app/metadata/filter/effect/collection.effect.ts @@ -102,6 +102,14 @@ export class FilterCollectionEffects { tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) ); + @Effect() + addFilterSuccessReloadParent$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), + map(action => action.payload), + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + map(([filter, provider]) => new SelectProviderRequest(provider)) + ); + @Effect() updateFilter$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST), diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.html b/ui/src/app/metadata/provider/container/provider-edit-step.component.html index bab3e5b90..ad969fd9e 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.html +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.html @@ -10,6 +10,7 @@ [schema]="schema$ | async" [model]="model$ | async" [validators]="validators$ | async" + [bindings]="bindings$ | async" (onChange)="valueChangeSubject.next($event)" (onErrorChange)="statusChangeSubject.next($event)"> \ No newline at end of file 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 2a32d925d..be79bb1fa 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 @@ -37,6 +37,7 @@ export class ProviderEditStepComponent implements OnDestroy { definition$: Observable>; changes$: Observable; step$: Observable; + bindings$: Observable; validators$: Observable<{ [key: string]: any }>; @@ -50,6 +51,7 @@ export class ProviderEditStepComponent implements OnDestroy { this.provider$ = this.store.select(fromProvider.getSelectedProvider); this.step$ = this.store.select(fromWizard.getCurrent); this.schema$ = this.store.select(fromWizard.getSchema); + this.bindings$ = this.definition$.pipe(map(d => d.bindings)); this.step$.subscribe(s => { if (s && s.locked) { @@ -99,9 +101,18 @@ export class ProviderEditStepComponent implements OnDestroy { this.valueChangeEmitted$.pipe( map(changes => changes.value), - withLatestFrom(this.definition$), - skipWhile(([ changes, definition ]) => !definition || !changes), - map(([ changes, definition ]) => definition.parser(changes)) + withLatestFrom(this.definition$, this.store.select(fromProvider.getSelectedProvider)), + filter(([ changes, definition, provider ]) => definition && changes && provider), + map(([ changes, definition, provider ]) => { + const parsed = definition.parser(changes); + return ({ + ...parsed, + metadataFilters: [ + ...provider.metadataFilters, + ...(parsed.metadataFilters || []) + ] + }); + }) ) .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); diff --git a/ui/src/app/metadata/provider/container/provider-select.component.ts b/ui/src/app/metadata/provider/container/provider-select.component.ts index baba7f626..8fe540eeb 100644 --- a/ui/src/app/metadata/provider/container/provider-select.component.ts +++ b/ui/src/app/metadata/provider/container/provider-select.component.ts @@ -31,6 +31,8 @@ export class ProviderSelectComponent implements OnDestroy { map(params => new SelectProviderRequest(params.providerId)) ).subscribe(store); + this.route.params.subscribe(params => console.log(params)); + this.provider$ = this.store.select(fromProviders.getSelectedProvider).pipe(skipWhile(p => !p)); this.provider$.subscribe(provider => this.setDefinition(provider)); diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.html b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html index 5d07730fd..a9a3be2e4 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.html +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html @@ -3,6 +3,7 @@ [schema]="schema$ | async" [model]="model$ | async" [validators]="validators$ | async" + [bindings]="bindings$ | async" (onChange)="valueChangeSubject.next($event)" (onErrorChange)="statusChangeSubject.next($event)"> \ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts index 9ff4ded23..a0b145078 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts @@ -28,6 +28,7 @@ export class ProviderWizardStepComponent implements OnDestroy { private statusChangeEmitted$ = this.statusChangeSubject.asObservable(); schema$: Observable; + bindings$: Observable; schema: any; definition$: Observable>; changes$: Observable; @@ -46,6 +47,7 @@ export class ProviderWizardStepComponent implements OnDestroy { filter(s => s && Object.keys(s.properties).length > 0) ); this.definition$ = this.store.select(fromWizard.getWizardDefinition); + this.bindings$ = this.definition$.pipe(map(d => d.bindings)); this.changes$ = this.store.select(fromProvider.getEntityChanges); this.validators$ = this.definition$.pipe( diff --git a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts new file mode 100644 index 000000000..7f38d02e3 --- /dev/null +++ b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts @@ -0,0 +1,140 @@ +import { FormProperty } from 'ngx-schema-form/lib/model/formproperty'; +import { ArrayProperty } from 'ngx-schema-form/lib/model/arrayproperty'; +import { ObjectProperty } from 'ngx-schema-form/lib/model/objectproperty'; + +import { Wizard } from '../../../wizard/model'; +import { DynamicHttpMetadataProvider } from '../../domain/model/providers/dynamic-http-metadata-provider'; +import { BaseMetadataProviderEditor } from './base.provider.form'; +import UriValidator from '../../../shared/validation/uri.validator'; + +export const DynamicHttpMetadataProviderWizard: Wizard = { + ...BaseMetadataProviderEditor, + label: 'DynamicHttpMetadataProvider', + type: 'DynamicHttpMetadataResolver', + bindings: {}, + 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; + }; + validators['/metadataURL'] = (value, property, form) => { + return !UriValidator.isUri(value) ? { + code: 'INVALID_URI', + path: `#${property.path}`, + message: 'message.uri-valid-format', + params: [value] + } : null; + }; + + return validators; + }, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'xmlId', + 'metadataURL', + 'requireValidMetadata', + 'failFastInitialization', + 'metadataRequestURLConstructionScheme' + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'dynamicMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 4, + initialValues: [ + { key: 'metadataFilters', value: [] } + ], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'metadataFilters' + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 5, + initialValues: [], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'enabled' + ] + } + ] +}; + + +export const DynamicHttpMetadataProviderEditor: Wizard = { + ...DynamicHttpMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'xmlId', + 'metadataRequestURLConstructionScheme', + 'enabled', + 'requireValidMetadata', + 'failFastInitialization' + ], + order: ['xmlId', 'metadataRequestURLConstructionScheme', 'enabled', 'requireValidMetadata', 'failFastInitialization'] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'dynamicMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 4, + initialValues: [ + { key: 'metadataFilters', value: [] } + ], + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'metadataFilters' + ] + }, + { + id: 'advanced', + label: 'label.http-settings-advanced', + index: 4, + initialValues: [], + locked: true, + schema: '/api/ui/MetadataResolver/DynamicHttpMetadataResolver', + fields: [ + 'httpMetadataResolverAttributes' + ] + } + ] +}; diff --git a/ui/src/app/metadata/provider/model/index.ts b/ui/src/app/metadata/provider/model/index.ts index 278652334..123f99812 100644 --- a/ui/src/app/metadata/provider/model/index.ts +++ b/ui/src/app/metadata/provider/model/index.ts @@ -1,19 +1,27 @@ import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form'; import { FileBackedHttpMetadataProviderEditor } from './file-backed-http.provider.form'; +import { DynamicHttpMetadataProviderWizard, DynamicHttpMetadataProviderEditor } from './dynamic-http.provider.form'; +import { LocalDynamicMetadataProviderWizard, LocalDynamicMetadataProviderEditor } from './local-dynamic.provider.form'; import { FileSystemMetadataProviderWizard, FileSystemMetadataProviderEditor } from './file-system.provider.form'; export const MetadataProviderWizardTypes = [ FileBackedHttpMetadataProviderWizard, - FileSystemMetadataProviderWizard + DynamicHttpMetadataProviderWizard, + FileSystemMetadataProviderWizard, + LocalDynamicMetadataProviderWizard ]; export const MetadataProviderEditorTypes = [ FileBackedHttpMetadataProviderEditor, + DynamicHttpMetadataProviderEditor, + LocalDynamicMetadataProviderEditor, FileSystemMetadataProviderEditor ]; export const FilterableProviders = [ - FileBackedHttpMetadataProviderEditor.type + FileBackedHttpMetadataProviderEditor.type, + DynamicHttpMetadataProviderEditor.type, + LocalDynamicMetadataProviderEditor.type ]; export * from './file-backed-http.provider.form'; diff --git a/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts b/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts new file mode 100644 index 000000000..4485409bc --- /dev/null +++ b/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts @@ -0,0 +1,141 @@ +import { Wizard } from '../../../wizard/model'; +import { LocalDynamicMetadataProvider } from '../../domain/model/providers/local-dynamic-metadata-provider'; +import { BaseMetadataProviderEditor } from './base.provider.form'; + +export const LocalDynamicMetadataProviderWizard: Wizard = { + ...BaseMetadataProviderEditor, + label: 'LocalDynamicMetadataProvider', + type: 'LocalDynamicMetadataResolver', + 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/LocalDynamicMetadataResolver', + fields: [ + 'xmlId', + 'sourceDirectory' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'sourceDirectory' + ] + } + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 3, + initialValues: [], + schema: '/api/ui/MetadataResolver/LocalDynamicMetadataResolver', + fields: [ + 'dynamicMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'dynamicMetadataResolverAttributes' + ] + } + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 4, + initialValues: [], + schema: '/api/ui/MetadataResolver/LocalDynamicMetadataResolver', + fields: [ + 'enabled' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled' + ] + } + ] + } + ] +}; + + +export const LocalDynamicMetadataProviderEditor: Wizard = { + ...LocalDynamicMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + schema: '/api/ui/MetadataResolver/LocalDynamicMetadataResolver', + fields: [ + 'name', + '@type', + 'enabled', + 'xmlId', + 'sourceDirectory', + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['LocalDynamicMetadataResolver'], description: 'value.local-dynamic-metadata-provider' }] + } + }, + fieldsets: [ + { + type: 'section', + class: ['mb-3'], + fields: [ + 'name', + '@type', + 'enabled' + ] + }, + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'sourceDirectory', + ] + } + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 2, + initialValues: [], + schema: '/api/ui/MetadataResolver/LocalDynamicMetadataResolver', + fields: [ + 'dynamicMetadataResolverAttributes' + ] + } + ] +}; diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index 1163fe889..b9a6a75d7 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -1,10 +1,12 @@
- +
+ +
- \ No newline at end of file + diff --git a/ui/src/app/schema-form/service/schema.service.spec.ts b/ui/src/app/schema-form/service/schema.service.spec.ts index 0dad960fe..eac84cd95 100644 --- a/ui/src/app/schema-form/service/schema.service.spec.ts +++ b/ui/src/app/schema-form/service/schema.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed, async, inject } from '@angular/core/testing'; -import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; -import { HttpClientModule, HttpRequest } from '@angular/common/http'; +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClientModule } from '@angular/common/http'; import { SchemaService } from './schema.service'; describe(`Schema Service`, () => { @@ -171,5 +171,91 @@ describe(`Schema Service`, () => { })).toBe(true); }) ); + + it(`should return true if dependency is active`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: { required: ['bar', 'baz'] }, + bar: { required: ['foo', 'baz'] }, + baz: { required: ['foo', 'bar'] } + } + }, + value: { + foo: 'abcdef' + } + }, + path: '/bar' + })).toBe(true); + }) + ); + + it(`should return true if the property has an active dependency`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: { required: ['bar', 'baz'] }, + bar: { required: ['foo', 'baz'] }, + baz: { required: ['foo', 'bar'] } + } + }, + value: { + foo: 'abc', + bar: '123' + } + }, + path: '/foo' + })).toBe(true); + }) + ); + + it(`should return false if no dependencies are defined`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + } + }, + value: { + foo: true, + baz: true + } + }, + path: '/bar' + })).toBe(false); + }) + ); + }); + + describe('getRequiredDependencies method', () => { + it('should return the provided result if an array', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies(['foo', 'bar'])).toEqual(['foo', 'bar']); + })); + + it('should return the content of the required attribute if provided', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies({required: ['foo', 'bar'] })).toEqual(['foo', 'bar']); + })); + + it('should return an empty array if not provided with required property', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies({ foo: 'bar' })).toEqual([]); + })); }); }); diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts index 2114ff283..9cd7346de 100644 --- a/ui/src/app/schema-form/service/schema.service.ts +++ b/ui/src/app/schema-form/service/schema.service.ts @@ -18,6 +18,7 @@ export class SchemaService { if (!formProperty || !formProperty.parent) { return false; } + let requiredFields = formProperty.parent.schema.required || []; let fieldPath = formProperty.path; let controlName = fieldPath.substr(fieldPath.lastIndexOf('/') + 1); @@ -38,6 +39,28 @@ export class SchemaService { required = !required ? requiredFields.indexOf(controlName) > -1 : required; }); } + + if (!required && formProperty.parent instanceof Object) { + const parent = formProperty.parent; + const dependencies = parent.schema.dependencies; + if (dependencies) { + const isDependencyOf = Object.keys(dependencies).filter(d => { + let dep = dependencies[d]; + return this.getRequiredDependencies(dep); + }); + const hasActiveDependencies = dependencies.hasOwnProperty(controlName) && + this.getRequiredDependencies(dependencies[controlName]).filter( + d => parent.value.hasOwnProperty(d) + ); + const isRequired = isDependencyOf.some(d => parent.value.hasOwnProperty(d) && !!parent.value[d]); + required = isRequired || !!hasActiveDependencies.length; + } + } + return required; } + + getRequiredDependencies(dep: any): string[] { + return (dep instanceof Array) ? dep : dep.hasOwnProperty('required') ? dep.required : []; + } } diff --git a/ui/src/app/schema-form/widget/array/array.component.html b/ui/src/app/schema-form/widget/array/array.component.html index ee986eea2..93fd94525 100644 --- a/ui/src/app/schema-form/widget/array/array.component.html +++ b/ui/src/app/schema-form/widget/array/array.component.html @@ -12,6 +12,12 @@ + + + , + {{ error.message }} + +
    ; + hasErrors: boolean; + hasErrorSub: Subscription; + + messages = { + ARRAY_UNIQUE: 'message.array-items-must-be-unique' + }; + + ngAfterViewInit(): void { + this.errors$ = this.formProperty.errorsChanges.pipe( + map(errors => errors ? errors.filter(err => err.code !== 'UNRESOLVABLE_REFERENCE').reduce((coll, err) => { + coll[err.code] = err; + return coll; + }, {}) : {}), + map(collection => Object.values(collection)) + ); + + this.hasErrorSub = this.errors$.subscribe(e => this.hasErrors = !!e.length); + } + + ngOnDestroy(): void { + this.hasErrorSub.unsubscribe(); + } getListType(property: any): string { return property.properties.length ? property.properties[0].type : null; } diff --git a/ui/src/app/schema-form/widget/object/object.component.ts b/ui/src/app/schema-form/widget/object/object.component.ts index 8850e7a74..54445c30e 100644 --- a/ui/src/app/schema-form/widget/object/object.component.ts +++ b/ui/src/app/schema-form/widget/object/object.component.ts @@ -7,4 +7,4 @@ import { ObjectWidget } from 'ngx-schema-form'; selector: 'custom-object', templateUrl: `./object.component.html` }) -export class CustomObjectWidget extends ObjectWidget { } +export class CustomObjectWidget extends ObjectWidget {} diff --git a/ui/src/app/schema-form/widget/string/string.component.html b/ui/src/app/schema-form/widget/string/string.component.html index e8dd0001a..26964a59f 100644 --- a/ui/src/app/schema-form/widget/string/string.component.html +++ b/ui/src/app/schema-form/widget/string/string.component.html @@ -1,7 +1,9 @@