diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d81aedbff0bfe574964db1a86e6bd0ca6ed290..9d2d7883229dd069113850e5512a34b7d8b737ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # 2.0.14 +* [LGR-75](https://jira.software.geant.org/browse/LGR-73) - Externalise saml groups to role * [LGR-73](https://jira.software.geant.org/browse/LGR-73) - Remove router endpoint * [LGR-72](https://jira.software.geant.org/browse/LGR-72) - Check user is authenticated prior to executing commands on internal routers diff --git a/src/main/java/org/geant/lgservice/config/CommandsParser.java b/src/main/java/org/geant/lgservice/config/CommandsParser.java index f3051fa7b626ae16504d368d797dde0c84d80012..1f8ee9526713d86b39af8ccafa8808d2717164b8 100644 --- a/src/main/java/org/geant/lgservice/config/CommandsParser.java +++ b/src/main/java/org/geant/lgservice/config/CommandsParser.java @@ -1,77 +1,61 @@ package org.geant.lgservice.config; -import com.google.common.collect.ImmutableMap; -import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.geant.lgservice.exceptions.TechnicalException; import org.geant.lgservice.pojos.Group; import org.geant.lgservice.security.LGUser; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.InputStream; -import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.function.Predicate; +import java.util.Optional; public class CommandsParser { - private static final Logger logger = LogManager.getLogger(CommandsParser.class); - - public List<Group> getCommandsFromXML(final String fileName, final LGUser user) { - Commands commands = new Commands(); - try { - String newFileName = resolveFileName(fileName, user); - if (newFileName != null) { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream stream = classLoader.getResourceAsStream(newFileName); - - JAXBContext jaxbContext = JAXBContext.newInstance(Commands.class); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - commands = (Commands) unmarshaller.unmarshal(stream); - } else { - logger.error("Unable to read config file for commands list"); - } - } catch (JAXBException e) { - logger.error("Unable to parse xml for " + fileName, e); - } - - return commands.getGroups(); - } - - @Builder - @Getter - private static class FileName { - private final String accessLevel; - private final int rank; - } - - private static final Map<Predicate<Collection<GrantedAuthority>>, FileName> FILE_NAME_PREDICATES = ImmutableMap - .<Predicate<Collection<GrantedAuthority>>, FileName>builder() - .put( - grantedAuthorities -> grantedAuthorities.contains(LGUser.Group.INTERNAL), - FileName.builder().accessLevel("internal").rank(0).build()) - .put( - grantedAuthorities -> grantedAuthorities.contains(LGUser.Group.EXTERNAL), - FileName.builder().accessLevel("external").rank(1).build()) - .build(); - - public String resolveFileName(final String fileName, final LGUser user) { - String accessLevel = "public"; - if (user != null) { - accessLevel = FILE_NAME_PREDICATES.entrySet() - .stream() - .filter(entry -> entry.getKey().test(user.getAuthorities())) - .map(Map.Entry::getValue) - .reduce((fileName1, fileName2) -> fileName1.getRank() < fileName2.getRank() ? fileName1 : fileName2) - .map(FileName::getAccessLevel) - .orElse("public"); - } - - return String.format("%s_%s.xml", fileName, accessLevel); - } + private static final Logger logger = LogManager.getLogger(CommandsParser.class); + + public List<Group> getCommandsFromXML(final String fileName, final LGUser user) { + try { + InputStream stream = getClass().getClassLoader().getResourceAsStream(resolveFileName(fileName, user)); + + JAXBContext jaxbContext = JAXBContext.newInstance(Commands.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + return ((Commands) unmarshaller.unmarshal(stream)).getGroups(); + } catch (JAXBException e) { + logger.error("Unable to parse xml for " + fileName, e); + throw new TechnicalException(e); + } + + } + + @RequiredArgsConstructor + private enum GroupRanking { + INTERNAL(0), + EXTERNAL(1); + + @Getter + private final int rank; + } + + public String resolveFileName(final String fileName, final LGUser user) { + String accessLevel = Optional.ofNullable(user) + .map(UserDetails::getAuthorities) + .flatMap(authorities -> authorities + .stream() + .map(GrantedAuthority::getAuthority) + .map(GroupRanking::valueOf) + .reduce((groupOne, groupTwo) -> groupOne.getRank() < groupTwo.getRank() ? groupOne : groupTwo)) + .map(GroupRanking::name) + .map(String::toLowerCase) + .orElse("public"); + + return String.format("%s_%s.xml", fileName, accessLevel); + } } diff --git a/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java index 02003fac615c7070ee22e8bb99cf67f1915a5623..17ac48d40a7878533f62cffb126e8d02c7bebc28 100644 --- a/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java +++ b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java @@ -9,7 +9,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import okhttp3.Interceptor; import okhttp3.OkHttpClient; -import okhttp3.Request; import org.geant.lgservice.domain.Router; import org.geant.lgservice.domain.RouterRepository; import org.geant.lgservice.exceptions.TechnicalException; diff --git a/src/main/java/org/geant/lgservice/security/LGUser.java b/src/main/java/org/geant/lgservice/security/LGUser.java index 340631cda86d2750ae7dead3b22ef295621bcee7..4aa362ed5a388b9a2060e2203bb54ad35c542b31 100644 --- a/src/main/java/org/geant/lgservice/security/LGUser.java +++ b/src/main/java/org/geant/lgservice/security/LGUser.java @@ -3,95 +3,67 @@ package org.geant.lgservice.security; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Singular; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -import java.util.Optional; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toSet; +@Builder @Getter public class LGUser implements UserDetails { - private final String federatedUser; - private final String givenName; - private final String surname; - private final String mail; - private final Collection<GrantedAuthority> authorities; - - @RequiredArgsConstructor - public enum Group implements GrantedAuthority{ - INTERNAL(new SimpleGrantedAuthority("GEANT Staff:All")), - EXTERNAL(new SimpleGrantedAuthority("GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs")); - - private final GrantedAuthority authority; - - public static Optional<Group> fromGroup(String group) { - return Stream.of(Group.values()) - .filter(value -> value.authority.getAuthority().equals(group)) - .findFirst(); - } - - public static Optional<Group> fromAuthority(GrantedAuthority authority) { - return Stream.of(Group.values()) - .filter(authority::equals) - .findFirst(); - } - - @Override - public String getAuthority() { - return name(); - } - } - - @Builder - public LGUser(String federatedUser, String givenName, String surname, String mail, - String ...groups) { - - this.federatedUser = federatedUser; - this.givenName = givenName; - this.surname = surname; - this.mail = mail; - this.authorities = Stream.of(groups) - .map(Group::fromGroup) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(toSet()); - - - } - - @Override - public String getPassword() { - return null; - } - - @Override - public String getUsername() { - return federatedUser; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } + private final String federatedUser; + private final String givenName; + private final String surname; + private final String mail; + @Singular + private final Collection<Group> groups; + + @RequiredArgsConstructor + public enum Group implements GrantedAuthority{ + INTERNAL, + EXTERNAL; + + @Override + public String getAuthority() { + return name(); + } + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return getGroups(); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return federatedUser; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } } diff --git a/src/main/java/org/geant/lgservice/security/LGUserService.java b/src/main/java/org/geant/lgservice/security/LGUserService.java index a0ac88e9bc512327909cdaf52c8f52d6fad5358e..6949949ab3c245652e87b76e2136cff59bc10c38 100644 --- a/src/main/java/org/geant/lgservice/security/LGUserService.java +++ b/src/main/java/org/geant/lgservice/security/LGUserService.java @@ -1,5 +1,7 @@ package org.geant.lgservice.security; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -7,26 +9,47 @@ import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.userdetails.SAMLUserDetailsService; import org.springframework.stereotype.Service; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.geant.lgservice.security.SamlKeyMappings.Key.EMAIL; +import static org.geant.lgservice.security.SamlKeyMappings.Key.GIVEN_NAME; +import static org.geant.lgservice.security.SamlKeyMappings.Key.GROUPS; +import static org.geant.lgservice.security.SamlKeyMappings.Key.ID; +import static org.geant.lgservice.security.SamlKeyMappings.Key.SURNAME; + @Service +@RequiredArgsConstructor +@Slf4j public class LGUserService implements SAMLUserDetailsService { - private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class); - - @Override - public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - String federatedUser = credential.getAttributeAsString("uid"); - String givenName = credential.getAttributeAsString("givenName"); - String surname = credential.getAttributeAsString("sn"); - String mail = credential.getAttributeAsString("mail"); - String[] groupNames = credential.getAttributeAsStringArray("isMemberOf"); - - LOGGER.info("Current user : " + givenName + ", " + surname); - return LGUser.builder() - .federatedUser(federatedUser) - .givenName(givenName) - .surname(surname) - .mail(mail) - .groups(groupNames) - .build(); - } + private final Map<SamlKeyMappings.Key, String> keyMappings; + private final Map<String, LGUser.Group> groupMappings; + + @Override + public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { + log.debug("Extracting attributes using keys: {}", keyMappings); + + String federatedUser = credential.getAttributeAsString(keyMappings.get(ID)); + String givenName = credential.getAttributeAsString(keyMappings.get(GIVEN_NAME)); + String surname = credential.getAttributeAsString(keyMappings.get(SURNAME)); + String mail = credential.getAttributeAsString(keyMappings.get(EMAIL)); + + Collection<LGUser.Group> groups = Arrays.stream(credential.getAttributeAsStringArray(keyMappings.get(GROUPS))) + .map(groupMappings::get) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + log.info("Current user : " + givenName + ", " + surname); + return LGUser.builder() + .federatedUser(federatedUser) + .givenName(givenName) + .surname(surname) + .mail(mail) + .groups(groups) + .build(); + } } diff --git a/src/main/java/org/geant/lgservice/security/SamlGroupMapping.java b/src/main/java/org/geant/lgservice/security/SamlGroupMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..4bdbd6ded72d096807f2f740d6ec74fe51e5e960 --- /dev/null +++ b/src/main/java/org/geant/lgservice/security/SamlGroupMapping.java @@ -0,0 +1,24 @@ +package org.geant.lgservice.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Collections.unmodifiableMap; + +@ConstructorBinding +@ConfigurationProperties("saml") +public class SamlGroupMapping { + private final Map<LGUser.Group, String> groupMappings; + + public SamlGroupMapping(Map<LGUser.Group, String> groupMappings) { + this.groupMappings = groupMappings; + } + + public Map<String, LGUser.Group> groupMappings() { + return unmodifiableMap(groupMappings.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey))); + } +} diff --git a/src/main/java/org/geant/lgservice/security/SamlKeyMappings.java b/src/main/java/org/geant/lgservice/security/SamlKeyMappings.java new file mode 100644 index 0000000000000000000000000000000000000000..7c2551775e4800c7cd89ad6d90ff4bd1f953b64b --- /dev/null +++ b/src/main/java/org/geant/lgservice/security/SamlKeyMappings.java @@ -0,0 +1,38 @@ +package org.geant.lgservice.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +import java.util.Arrays; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.Collections.unmodifiableMap; + +@ConstructorBinding +@ConfigurationProperties("saml") +public class SamlKeyMappings { + + public enum Key { + ID, + GIVEN_NAME, + SURNAME, + EMAIL, + GROUPS + } + + private final Map<Key, String> keyMappings; + + public SamlKeyMappings(Map<Key, String> keyMappings) { + checkNotNull(keyMappings, "Missing saml key mappings, under 'saml.key-mappings'"); + checkArgument(keyMappings.size() == Key + .values().length, format("Missing saml key mappings, required: %s", Arrays.toString(Key.values()))); + this.keyMappings = keyMappings; + } + + public Map<Key, String> mappings() { + return unmodifiableMap(keyMappings); + } +} diff --git a/src/main/java/org/geant/lgservice/security/WebSecurityConfig.java b/src/main/java/org/geant/lgservice/security/WebSecurityConfig.java index 2f27ad859d0c82e997cdf4d6212a5f343706c5f9..349490dd5ea24c1088569d13f1d4bb2aacb4f953 100644 --- a/src/main/java/org/geant/lgservice/security/WebSecurityConfig.java +++ b/src/main/java/org/geant/lgservice/security/WebSecurityConfig.java @@ -11,6 +11,7 @@ import org.opensaml.xml.parse.StaticBasicParserPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.DefaultResourceLoader; @@ -71,65 +72,70 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.*; +import java.util.Timer; import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) +@EnableConfigurationProperties({ + SamlGroupMapping.class, + SamlKeyMappings.class +}) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - - @Autowired - AppConfig config; - - private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class); - - private String metadataURL; - - private Timer backgroundTaskTimer; - private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager; - - @PostConstruct - public void init() { - this.backgroundTaskTimer = new Timer(true); - this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager(); - this.metadataURL = config.getMetadataURL(); - } - - @PreDestroy - public void destroy() { - this.backgroundTaskTimer.purge(); - this.backgroundTaskTimer.cancel(); - this.multiThreadedHttpConnectionManager.shutdown(); - } - + + @Autowired + AppConfig config; + + private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class); + + private String metadataURL; + + private Timer backgroundTaskTimer; + private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager; + + @PostConstruct + public void init() { + this.backgroundTaskTimer = new Timer(true); + this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager(); + this.metadataURL = config.getMetadataURL(); + } + + @PreDestroy + public void destroy() { + this.backgroundTaskTimer.purge(); + this.backgroundTaskTimer.cancel(); + this.multiThreadedHttpConnectionManager.shutdown(); + } + @Autowired private LGUserService userService; - + @Bean public VelocityEngine velocityEngine() { return VelocityFactory.getEngine(); } - + @Bean(initMethod = "initialize") public StaticBasicParserPool parserPool() { return new StaticBasicParserPool(); } - + @Bean(name = "parserPoolHolder") public ParserPoolHolder parserPoolHolder() { return new ParserPoolHolder(); } - + // Bindings, encoders and decoders used for creating and parsing messages @Bean - public HttpClient httpClient() { - HttpClient httpClient = new HttpClient(this.multiThreadedHttpConnectionManager); - httpClient.getHostConfiguration().setHost( - fromHttpUrl(metadataURL).build().toUri().getHost()); - return httpClient; - } - + public HttpClient httpClient() { + HttpClient httpClient = new HttpClient(this.multiThreadedHttpConnectionManager); + httpClient.getHostConfiguration().setHost( + fromHttpUrl(metadataURL).build().toUri().getHost()); + return httpClient; + } + // SAML Authentication Provider responsible for validating of received SAML // messages @Bean @@ -139,77 +145,77 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { samlAuthenticationProvider.setForcePrincipalAsString(false); return samlAuthenticationProvider; } - + // Provider of default SAML Context @Bean public SAMLContextProviderImpl contextProvider() { return new SAMLContextProviderImpl(); } - + // Initialization of OpenSAML library @Bean public static SAMLBootstrap sAMLBootstrap() { return new SAMLBootstrap(); } - + // Logger for SAML messages and events @Bean public SAMLDefaultLogger samlLogger() { return new SAMLDefaultLogger(); } - + // SAML 2.0 WebSSO Assertion Consumer @Bean public WebSSOProfileConsumer webSSOprofileConsumer() { return new WebSSOProfileConsumerImpl(); } - + // SAML 2.0 Holder-of-Key WebSSO Assertion Consumer @Bean public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() { return new WebSSOProfileConsumerHoKImpl(); } - + // SAML 2.0 Web SSO profile @Bean public WebSSOProfile webSSOprofile() { return new WebSSOProfileImpl(); } - + // SAML 2.0 Holder-of-Key Web SSO profile @Bean public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() { return new WebSSOProfileConsumerHoKImpl(); } - + // SAML 2.0 ECP profile @Bean public WebSSOProfileECPImpl ecpprofile() { return new WebSSOProfileECPImpl(); } - + @Bean public SingleLogoutProfile logoutprofile() { return new SingleLogoutProfileImpl(); } - + @Bean - public KeyManager keyManager() { - DefaultResourceLoader loader = new DefaultResourceLoader(); - Resource storeFile = loader.getResource(config.getKeyStore()); - String storePass = config.getKeystorePassword(); - Map<String, String> passwords = new HashMap<String, String>(); - passwords.put(config.getKeystoreAlias(), config.getKeystorePassword()); - return new JKSKeyManager(storeFile, storePass, passwords, config.getKeystoreAlias()); - } - + public KeyManager keyManager() { + DefaultResourceLoader loader = new DefaultResourceLoader(); + Resource storeFile = loader.getResource(config.getKeyStore()); + String storePass = config.getKeystorePassword(); + Map<String, String> passwords = new HashMap<String, String>(); + passwords.put(config.getKeystoreAlias(), config.getKeystorePassword()); + return new JKSKeyManager(storeFile, storePass, passwords, config.getKeystoreAlias()); + } + @Bean public WebSSOProfileOptions defaultWebSSOProfileOptions() { WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions(); webSSOProfileOptions.setIncludeScoping(false); return webSSOProfileOptions; } - + // Entry point to initialize authentication, default values taken from // properties file @Bean @@ -218,19 +224,19 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions()); return samlEntryPoint; } - - // Setup advanced info about metadata - @Bean - public ExtendedMetadata extendedMetadata() { - ExtendedMetadata extendedMetadata = new ExtendedMetadata(); - extendedMetadata.setIdpDiscoveryEnabled(false); - extendedMetadata.setSignMetadata(false); - extendedMetadata.setEcpEnabled(false); - extendedMetadata.setSslHostnameVerification("allowAll"); - extendedMetadata.setSigningKey("spring"); - return extendedMetadata; - } - + + // Setup advanced info about metadata + @Bean + public ExtendedMetadata extendedMetadata() { + ExtendedMetadata extendedMetadata = new ExtendedMetadata(); + extendedMetadata.setIdpDiscoveryEnabled(false); + extendedMetadata.setSignMetadata(false); + extendedMetadata.setEcpEnabled(false); + extendedMetadata.setSslHostnameVerification("allowAll"); + extendedMetadata.setSigningKey("spring"); + return extendedMetadata; + } + // IDP Discovery Service @Bean public SAMLDiscovery samlIDPDiscovery() { @@ -238,46 +244,46 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { idpDiscovery.setIdpSelectionPath("/saml/idpSelection"); return idpDiscovery; } - - @Bean(name = "metadata") - public CachingMetadataManager metadata() throws MetadataProviderException { - - HTTPMetadataProvider provider = new HTTPMetadataProvider(this.backgroundTaskTimer, httpClient(), - this.metadataURL); - - //provider.setMinRefreshDelay(3600000); - //provider.setMinRefreshDelay(3600000); - provider.setParserPool(parserPool()); - - ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(provider, extendedMetadata()); - extendedMetadataDelegate.setMetadataTrustCheck(false); - extendedMetadataDelegate.setMetadataRequireSignature(false); - backgroundTaskTimer.purge(); - - List<MetadataProvider> providers = new ArrayList<MetadataProvider>(); - providers.add(extendedMetadataDelegate); - return new CachingMetadataManager(providers); - - } - - @Bean - public MetadataGenerator metadataGenerator() { - MetadataGenerator metadataGenerator = new MetadataGenerator(); - metadataGenerator.setEntityId(config.getEntityName()); - metadataGenerator.setEntityBaseURL(config.getEntityBaseURL()); - metadataGenerator.setExtendedMetadata(extendedMetadata()); - metadataGenerator.setIncludeDiscoveryExtension(false); - metadataGenerator.setKeyManager(keyManager()); - return metadataGenerator; - } - + + @Bean(name = "metadata") + public CachingMetadataManager metadata() throws MetadataProviderException { + + HTTPMetadataProvider provider = new HTTPMetadataProvider(this.backgroundTaskTimer, httpClient(), + this.metadataURL); + + //provider.setMinRefreshDelay(3600000); + //provider.setMinRefreshDelay(3600000); + provider.setParserPool(parserPool()); + + ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(provider, extendedMetadata()); + extendedMetadataDelegate.setMetadataTrustCheck(false); + extendedMetadataDelegate.setMetadataRequireSignature(false); + backgroundTaskTimer.purge(); + + List<MetadataProvider> providers = new ArrayList<MetadataProvider>(); + providers.add(extendedMetadataDelegate); + return new CachingMetadataManager(providers); + + } + + @Bean + public MetadataGenerator metadataGenerator() { + MetadataGenerator metadataGenerator = new MetadataGenerator(); + metadataGenerator.setEntityId(config.getEntityName()); + metadataGenerator.setEntityBaseURL(config.getEntityBaseURL()); + metadataGenerator.setExtendedMetadata(extendedMetadata()); + metadataGenerator.setIncludeDiscoveryExtension(false); + metadataGenerator.setKeyManager(keyManager()); + return metadataGenerator; + } + // The filter is waiting for connections on URL suffixed with filterSuffix // and presents SP metadata there @Bean public MetadataDisplayFilter metadataDisplayFilter() { return new MetadataDisplayFilter(); } - + // Handler deciding where to redirect user after successful login @Bean public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() { @@ -287,17 +293,17 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { LOGGER.info("executing the authentication success handler"); return successRedirectHandler; } - - // Handler deciding where to redirect user after failed login + + // Handler deciding where to redirect user after failed login @Bean public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() { - SimpleUrlAuthenticationFailureHandler failureHandler = - new SimpleUrlAuthenticationFailureHandler(); - failureHandler.setUseForward(true); - failureHandler.setDefaultFailureUrl("/error"); - return failureHandler; + SimpleUrlAuthenticationFailureHandler failureHandler = + new SimpleUrlAuthenticationFailureHandler(); + failureHandler.setUseForward(true); + failureHandler.setDefaultFailureUrl("/error"); + return failureHandler; } - + @Bean public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception { SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter(); @@ -306,7 +312,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); return samlWebSSOHoKProcessingFilter; } - + // Processing filter for WebSSO profile messages @Bean public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception { @@ -316,12 +322,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); return samlWebSSOProcessingFilter; } - + @Bean public MetadataGeneratorFilter metadataGeneratorFilter() { return new MetadataGeneratorFilter(metadataGenerator()); } - + // Handler for successful logout @Bean public SimpleUrlLogoutSuccessHandler successLogoutHandler() { @@ -329,17 +335,17 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { successLogoutHandler.setDefaultTargetUrl("/"); return successLogoutHandler; } - + // Logout handler terminating local session @Bean public SecurityContextLogoutHandler logoutHandler() { - SecurityContextLogoutHandler logoutHandler = - new SecurityContextLogoutHandler(); + SecurityContextLogoutHandler logoutHandler = + new SecurityContextLogoutHandler(); logoutHandler.setInvalidateHttpSession(true); logoutHandler.setClearAuthentication(true); return logoutHandler; } - + // Filter processing incoming logout messages // First argument determines URL user will be redirected to after successful // global logout @@ -348,59 +354,59 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler()); } - + // Overrides default logout processing filter with the one processing SAML // messages @Bean public SAMLLogoutFilter samlLogoutFilter() { return new SAMLLogoutFilter(successLogoutHandler(), - new LogoutHandler[] { logoutHandler() }, - new LogoutHandler[] { logoutHandler() }); + new LogoutHandler[]{logoutHandler()}, + new LogoutHandler[]{logoutHandler()}); } - + @Bean public HTTPSOAP11Binding soapBinding() { return new HTTPSOAP11Binding(parserPool()); } - + @Bean public HTTPPostBinding httpPostBinding() { - return new HTTPPostBinding(parserPool(), velocityEngine()); + return new HTTPPostBinding(parserPool(), velocityEngine()); } - + @Bean public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() { - return new HTTPRedirectDeflateBinding(parserPool()); + return new HTTPRedirectDeflateBinding(parserPool()); } - + @Bean public HTTPSOAP11Binding httpSOAP11Binding() { - return new HTTPSOAP11Binding(parserPool()); + return new HTTPSOAP11Binding(parserPool()); } - + @Bean public HTTPPAOS11Binding httpPAOS11Binding() { - return new HTTPPAOS11Binding(parserPool()); + return new HTTPPAOS11Binding(parserPool()); } - + // Processor - @Bean - public SAMLProcessorImpl processor() { - Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>(); - bindings.add(httpRedirectDeflateBinding()); - bindings.add(httpPostBinding()); - //bindings.add(artifactBinding(parserPool(), velocityEngine())); - bindings.add(httpSOAP11Binding()); - bindings.add(httpPAOS11Binding()); - return new SAMLProcessorImpl(bindings); - } - - /** - * Define the security filter chain in order to support SSO Auth by using SAML 2.0 - * - * @return Filter chain proxy - * @throws Exception - */ + @Bean + public SAMLProcessorImpl processor() { + Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>(); + bindings.add(httpRedirectDeflateBinding()); + bindings.add(httpPostBinding()); + //bindings.add(artifactBinding(parserPool(), velocityEngine())); + bindings.add(httpSOAP11Binding()); + bindings.add(httpPAOS11Binding()); + return new SAMLProcessorImpl(bindings); + } + + /** + * Define the security filter chain in order to support SSO Auth by using SAML 2.0 + * + * @return Filter chain proxy + * @throws Exception + */ @Bean public FilterChainProxy samlFilter() throws Exception { List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>(); @@ -420,57 +426,67 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { samlIDPDiscovery())); return new FilterChainProxy(chains); } - + /** * Returns the authentication manager currently used by Spring. * It represents a bean definition with the aim allow wiring from * other classes performing the Inversion of Control (IoC). - * - * @throws Exception + * + * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } - + /** * Defines the web based security configuration. - * - * @param http It allows configuring web based security for specific http requests. - * @throws Exception + * + * @param http It allows configuring web based security for specific http requests. + * @throws Exception */ - @Override + @Override protected void configure(HttpSecurity http) throws Exception { http - .httpBasic() + .httpBasic() .authenticationEntryPoint(samlEntryPoint()); http - .csrf() - .disable(); + .csrf() + .disable(); http - .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class) - .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class); - + .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class) + .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class); + http.authorizeRequests() - .antMatchers("/authenticate*/**").authenticated() - .anyRequest().permitAll(); - + .antMatchers("/authenticate*/**").authenticated() + .anyRequest().permitAll(); + http - .logout() + .logout() .logoutSuccessUrl("/"); } - + /** * Sets a custom authentication provider. - * - * @param auth SecurityBuilder used to create an AuthenticationManager. - * @throws Exception + * + * @param auth SecurityBuilder used to create an AuthenticationManager. + * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth - .authenticationProvider(samlAuthenticationProvider()); - } + .authenticationProvider(samlAuthenticationProvider()); + } + + @Bean + public Map<String, LGUser.Group> groupMappings(SamlGroupMapping mappings) { + return mappings.groupMappings(); + } + + @Bean + public Map<SamlKeyMappings.Key, String> keyMappings(SamlKeyMappings mappings) { + return mappings.mappings(); + } } \ No newline at end of file diff --git a/src/test/java/org/geant/lgservice/integration/scenario/Given.java b/src/test/java/org/geant/lgservice/integration/scenario/Given.java index 272ee36db013c224e86fb0226cd245fcd9d1cea9..6dc87eeb717e2c1707f1a50056f0ad706c6e0aff 100644 --- a/src/test/java/org/geant/lgservice/integration/scenario/Given.java +++ b/src/test/java/org/geant/lgservice/integration/scenario/Given.java @@ -21,9 +21,12 @@ import org.springframework.beans.factory.annotation.Autowired; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.util.Collections; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static org.geant.lgservice.integration.Context.withDefaults; +import static org.geant.lgservice.security.LGUser.Group.EXTERNAL; +import static org.geant.lgservice.security.LGUser.Group.INTERNAL; import static org.mockito.ArgumentMatchers.any; @JGivenStage @@ -59,23 +62,28 @@ public class Given extends ExtendedStage<Given> { } public GivenUser logged_in() { - context.withUser(new LGUser( - "1", - "barry", - "scott", - "barry.scott@geant.org", - "GEANT Staff:All")); + context.withUser( + LGUser.builder() + .federatedUser("1") + .givenName("barry") + .surname("scott") + .mail("barry.scott@geant.org") + .group(INTERNAL) + .group(EXTERNAL) + .build()); return self(); } public GivenUser does_not_have_internal_access() { LGUser user = context.getUser(); - context.withUser( new LGUser( - user.getFederatedUser(), - user.getGivenName(), - user.getSurname(), - user.getMail(), - "GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs")); + context.withUser( + LGUser.builder() + .federatedUser(user.getFederatedUser()) + .givenName(user.getGivenName()) + .surname(user.getSurname()) + .mail(user.getMail()) + .group(EXTERNAL) + .build()); return self(); } } diff --git a/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java b/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java index 156fe3c887fd294ba63f20cd323a34395e6633dd..503caa603154bffa8984e78ad745a427b4a40e7d 100644 --- a/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java +++ b/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java @@ -80,7 +80,7 @@ public class SubmitCommandRequestIT extends BaseIT<Given, When, Then> { given() .a().user() .that().is().logged_in() - .but().is().does_not_have_internal_access() + .but().does_not_have_internal_access() .backup() .and().a().router() .that().is().internal(); diff --git a/src/test/java/org/geant/lgservice/security/LGUserServiceTest.java b/src/test/java/org/geant/lgservice/security/LGUserServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..50f5332788d2478f5a67520d0ef15bee9a0735e6 --- /dev/null +++ b/src/test/java/org/geant/lgservice/security/LGUserServiceTest.java @@ -0,0 +1,71 @@ +package org.geant.lgservice.security; + +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.saml.SAMLCredential; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.geant.lgservice.security.LGUser.Group.EXTERNAL; +import static org.geant.lgservice.security.LGUser.Group.INTERNAL; +import static org.geant.lgservice.security.SamlKeyMappings.Key.EMAIL; +import static org.geant.lgservice.security.SamlKeyMappings.Key.GIVEN_NAME; +import static org.geant.lgservice.security.SamlKeyMappings.Key.GROUPS; +import static org.geant.lgservice.security.SamlKeyMappings.Key.ID; +import static org.geant.lgservice.security.SamlKeyMappings.Key.SURNAME; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +public class LGUserServiceTest { + + SAMLCredential credential = mock(SAMLCredential.class); + + LGUserService subject = new LGUserService( + ImmutableMap.<SamlKeyMappings.Key, String>builder() + .put(ID, "uid") + .put(GIVEN_NAME, "givenName") + .put(SURNAME, "sn") + .put(EMAIL, "mail") + .put(GROUPS, "isMemberOf") + .build(), + ImmutableMap.<String, LGUser.Group>builder() + .put("internal", INTERNAL) + .put("external", EXTERNAL) + .build()); + + @Before + public void setup() { + reset(credential); + when(credential.getAttributeAsString("uid")).thenReturn("1"); + when(credential.getAttributeAsString("givenName")).thenReturn("barry"); + when(credential.getAttributeAsString("sn")).thenReturn("scott"); + when(credential.getAttributeAsString("mail")).thenReturn("barry.scott@geant.org"); + } + + @Test + public void loadUserWithMultipleGroups() { + when(credential.getAttributeAsStringArray("isMemberOf")) + .thenReturn(new String[]{"internal", "external", "ignore"}); + + LGUser result = (LGUser) subject.loadUserBySAML(credential); + assertThat(result.getFederatedUser()).isEqualTo("1"); + assertThat(result.getGivenName()).isEqualTo("barry"); + assertThat(result.getSurname()).isEqualTo("scott"); + assertThat(result.getMail()).isEqualTo("barry.scott@geant.org"); + assertThat(result.getAuthorities()).containsExactlyInAnyOrder(INTERNAL, EXTERNAL); + } + + @Test + public void loadUser() { + when(credential.getAttributeAsStringArray("isMemberOf")) + .thenReturn(new String[]{"external", "ignore"}); + + LGUser result = (LGUser) subject.loadUserBySAML(credential); + assertThat(result.getFederatedUser()).isEqualTo("1"); + assertThat(result.getGivenName()).isEqualTo("barry"); + assertThat(result.getSurname()).isEqualTo("scott"); + assertThat(result.getMail()).isEqualTo("barry.scott@geant.org"); + assertThat(result.getAuthorities()).containsExactlyInAnyOrder(EXTERNAL); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 3ceaa22a4b8c1e14e93d328803c1aaf7408735ab..4ff7a719f4e6ea3c6b147748b34a8c8113c8263a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -17,3 +17,12 @@ saml: key-store: classpath:saml/keystore.jks key-alias: spring key-store-password: secret + key-mappings: + id: "id" + given-name: "givenName" + surname: "surname" + email: "mail" + groups: "isMemberOf" + group-mappings: + INTERNAL: "GEANT Staff:All" + EXTERNAL: "GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs"