From 52b39b7a4a374ef185f80dbbb1120b51f0bf98ff Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 28 Nov 2023 16:11:51 -0600 Subject: [PATCH] feat(saml): Update SAML to use Spring Security This updates `gate-saml` to use the built-in SAML support in Spring Security which uses OpenSAML 4.x. Nearly all the previously supported properties for SAML are still supported, though a couple niche options no longer seem to be configurable. This also introduces `AuthenticationService`, a variation of `PermissionService` which can also return a user's granted authorities in one login call. It was also used for exception translation previously as retrofit exceptions are not serializable which would cause errors in Spring Security authentication failure error handlers, but the underlying exception being thrown has since been updated to avoid that problem. --- .../gate/services/AuthenticationService.java | 100 ++++++++ gate-saml/gate-saml.gradle | 23 +- .../saml/DefaultUserIdentifierExtractor.java | 36 +++ .../saml/DefaultUserRolesExtractor.java | 82 +++++++ .../saml/ResponseAuthenticationConverter.java | 95 ++++++++ .../gate/security/saml/SAMLConfiguration.java | 107 ++++++++ .../gate/security/saml/SAMLSSOConfig.java | 125 ---------- .../security/saml/SAMLUserDetailsService.java | 228 ------------------ ...rties.java => SecuritySamlProperties.java} | 147 +++++------ .../saml/UserIdentifierExtractor.java | 25 ++ .../security/saml/UserRolesExtractor.java | 27 +++ .../gate/security/saml/package-info.java | 21 ++ .../SAMLSecurityConfigPropertiesSpec.groovy | 55 ----- gate-saml/src/test/resources/saml-client.jks | Bin 1300 -> 0 bytes 14 files changed, 583 insertions(+), 488 deletions(-) create mode 100644 gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java delete mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java delete mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java rename gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/{SAMLSecurityConfigProperties.java => SecuritySamlProperties.java} (50%) create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java create mode 100644 gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java delete mode 100644 gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy delete mode 100644 gate-saml/src/test/resources/saml-client.jks diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java new file mode 100644 index 0000000000..14773db1b5 --- /dev/null +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/AuthenticationService.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.services; + +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.fiat.shared.FiatService; +import com.netflix.spinnaker.fiat.shared.FiatStatus; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import io.micrometer.core.annotation.Counted; +import java.util.Collection; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** Facade for logging in an authenticated user and obtaining Fiat authorities. */ +@Log4j2 +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final FiatStatus fiatStatus; + private final FiatService fiatService; + private final FiatPermissionEvaluator permissionEvaluator; + + @Setter( + onParam_ = {@Qualifier("fiatLoginService")}, + onMethod_ = {@Autowired(required = false)}) + private FiatService fiatLoginService; + + private FiatService getFiatServiceForLogin() { + return fiatLoginService != null ? fiatLoginService : fiatService; + } + + @Counted("fiat.login") + public Collection login(String userid) { + if (!fiatStatus.isEnabled()) { + return Set.of(); + } + + return AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginUser(userid, ""); + return resolveAuthorities(userid); + }); + } + + @Counted("fiat.login") + public Collection loginWithRoles( + String userid, Collection roles) { + if (!fiatStatus.isEnabled()) { + return Set.of(); + } + + return AuthenticatedRequest.allowAnonymous( + () -> { + getFiatServiceForLogin().loginWithRoles(userid, roles); + return resolveAuthorities(userid); + }); + } + + @Counted("fiat.logout") + public void logout(String userid) { + if (!fiatStatus.isEnabled()) { + return; + } + + getFiatServiceForLogin().logoutUser(userid); + permissionEvaluator.invalidatePermission(userid); + } + + private Collection resolveAuthorities(String userid) { + permissionEvaluator.invalidatePermission(userid); + var permission = permissionEvaluator.getPermission(userid); + if (permission == null) { + throw new UsernameNotFoundException( + String.format("No user found in Fiat named '%s'", userid)); + } + return permission.toGrantedAuthorities(); + } +} diff --git a/gate-saml/gate-saml.gradle b/gate-saml/gate-saml.gradle index 18dda69573..311f2b668d 100644 --- a/gate-saml/gate-saml.gradle +++ b/gate-saml/gate-saml.gradle @@ -1,13 +1,20 @@ -dependencies{ +dependencies { + constraints { + implementation 'org.opensaml:opensaml-core:4.1.0' + implementation 'org.opensaml:opensaml-saml-api:4.1.0' + implementation 'org.opensaml:opensaml-saml-impl:4.1.0' + } + implementation project(':gate-core') - // RetrySupport is in kork-exceptions and not kork-core! + implementation 'io.spinnaker.kork:kork-core' + implementation 'io.spinnaker.kork:kork-crypto' + implementation 'io.spinnaker.kork:kork-exceptions' + implementation 'io.spinnaker.kork:kork-security' implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" - implementation "io.spinnaker.kork:kork-exceptions" - implementation "io.spinnaker.kork:kork-security" - implementation 'org.springframework:spring-context' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.security:spring-security-saml2-service-provider' implementation 'org.springframework.session:spring-session-core' - implementation 'org.springframework.boot:spring-boot-autoconfigure' - implementation "org.springframework.security.extensions:spring-security-saml2-core" - implementation "org.springframework.security.extensions:spring-security-saml-dsl-core" + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java new file mode 100644 index 0000000000..65d3a994d8 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserIdentifierExtractor.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** + * Default implementation for extracting the user id from an authenticated SAML user. This uses the + * settings in {@link SecuritySamlProperties.UserAttributeMapping#getUsername()} + */ +@RequiredArgsConstructor +public class DefaultUserIdentifierExtractor implements UserIdentifierExtractor { + private final SecuritySamlProperties properties; + + @Override + public String fromPrincipal(Saml2AuthenticatedPrincipal principal) { + String userid = principal.getFirstAttribute(properties.getUserAttributeMapping().getUsername()); + return userid != null ? userid : principal.getName(); + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java new file mode 100644 index 0000000000..15f947e7b7 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/DefaultUserRolesExtractor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.kork.exceptions.ConfigurationException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import lombok.RequiredArgsConstructor; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** + * Default implementation for extracting roles from an authenticated SAML user. This uses the + * settings in {@link SecuritySamlProperties} related to roles. If role names appear to be + * distinguished names (i.e., they contain the substring {@code CN=}), then they will be parsed as + * DNs to extract the common name (CN) attribute. + */ +@RequiredArgsConstructor +public class DefaultUserRolesExtractor implements UserRolesExtractor { + private final SecuritySamlProperties properties; + + @Override + public Set getRoles(Saml2AuthenticatedPrincipal principal) { + var userAttributeMapping = properties.getUserAttributeMapping(); + List roles = principal.getAttribute(userAttributeMapping.getRoles()); + Stream roleStream = roles != null ? roles.stream() : Stream.empty(); + String delimiter = userAttributeMapping.getRolesDelimiter(); + roleStream = + delimiter != null + ? roleStream.flatMap(role -> Stream.of(role.split(delimiter))) + : roleStream; + roleStream = roleStream.map(DefaultUserRolesExtractor::parseRole); + if (properties.isForceLowercaseRoles()) { + roleStream = roleStream.map(role -> role.toLowerCase(Locale.ROOT)); + } + if (properties.isSortRoles()) { + roleStream = roleStream.sorted(); + } + return roleStream.collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static String parseRole(String role) { + if (!role.contains("CN=")) { + return role; + } + try { + return new LdapName(role) + .getRdns().stream() + .filter(rdn -> rdn.getType().equals("CN")) + .map(rdn -> (String) rdn.getValue()) + .findFirst() + .orElseThrow( + () -> + new ConfigurationException( + String.format( + "SAML role '%s' contains 'CN=' but cannot be parsed as a DN", role))); + } catch (InvalidNameException e) { + throw new ConfigurationException( + String.format("Unable to parse SAML role name '%s'", role), e); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java new file mode 100644 index 0000000000..3887fd1428 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/ResponseAuthenticationConverter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.gate.services.AuthenticationService; +import com.netflix.spinnaker.security.User; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.CollectionUtils; + +/** Handles conversion of an authenticated SAML user into a Spinnaker user and populating Fiat. */ +@Log4j2 +@RequiredArgsConstructor +public class ResponseAuthenticationConverter + implements Converter { + private final SecuritySamlProperties properties; + private final ObjectFactory userIdentifierExtractorFactory; + private final ObjectFactory userRolesExtractorFactory; + private final ObjectFactory authenticationServiceFactory; + + @Override + public PreAuthenticatedAuthenticationToken convert(ResponseToken source) { + UserIdentifierExtractor userIdentifierExtractor = userIdentifierExtractorFactory.getObject(); + UserRolesExtractor userRolesExtractor = userRolesExtractorFactory.getObject(); + AuthenticationService loginService = authenticationServiceFactory.getObject(); + log.debug("Decoding SAML response: {}", source.getToken()); + + Saml2Authentication authentication = convertToken(source); + @SuppressWarnings("deprecation") + var user = new User(); + Saml2AuthenticatedPrincipal principal = + (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + String principalName = principal.getName(); + var userAttributeMapping = properties.getUserAttributeMapping(); + String email = principal.getFirstAttribute(userAttributeMapping.getEmail()); + user.setEmail(email != null ? email : principalName); + String userid = userIdentifierExtractor.fromPrincipal(principal); + user.setUsername(userid); + user.setFirstName(principal.getFirstAttribute(userAttributeMapping.getFirstName())); + user.setLastName(principal.getFirstAttribute(userAttributeMapping.getLastName())); + + Set roles = userRolesExtractor.getRoles(principal); + user.setRoles(roles); + + if (!CollectionUtils.isEmpty(properties.getRequiredRoles())) { + var requiredRoles = Set.copyOf(properties.getRequiredRoles()); + // check for at least one common role in both sets + if (Collections.disjoint(roles, requiredRoles)) { + throw new BadCredentialsException( + String.format("User %s is not in any required role from %s", email, requiredRoles)); + } + } + + Collection authorities = loginService.loginWithRoles(userid, roles); + return new PreAuthenticatedAuthenticationToken(user, principal, authorities); + } + + private static final Converter DEFAULT_CONVERTER = + OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter(); + + private static Saml2Authentication convertToken(ResponseToken token) { + Saml2Authentication authentication = DEFAULT_CONVERTER.convert(token); + if (authentication == null) { + throw new IllegalArgumentException("Response token could not be converted"); + } + return authentication; + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java new file mode 100644 index 0000000000..7f3f1493a8 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.gate.config.AuthConfig; +import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; +import com.netflix.spinnaker.gate.services.AuthenticationService; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.session.DefaultCookieSerializerCustomizer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SecuritySamlProperties.class) +public class SAMLConfiguration { + + @EnableWebSecurity + @SpinnakerAuthConfig + @RequiredArgsConstructor + @ConditionalOnProperty("saml.enabled") + public static class WebSecurityConfig extends WebSecurityConfigurerAdapter { + private final SecuritySamlProperties properties; + private final AuthConfig authConfig; + private final ObjectProvider userIdentifierExtractorProvider; + private final ObjectProvider userRolesExtractorProvider; + private final ObjectFactory authenticationServiceFactory; + + /** Disables the same-site requirement for cookies as configured in other SSO modules. */ + @Bean + public static DefaultCookieSerializerCustomizer defaultCookieSerializerCustomizer() { + return cookieSerializer -> cookieSerializer.setSameSite(null); + } + + @Bean + public ResponseAuthenticationConverter responseAuthenticationConverter() { + return new ResponseAuthenticationConverter( + properties, + () -> + userIdentifierExtractorProvider.getIfAvailable( + () -> new DefaultUserIdentifierExtractor(properties)), + () -> + userRolesExtractorProvider.getIfAvailable( + () -> new DefaultUserRolesExtractor(properties)), + authenticationServiceFactory); + } + + @Bean + @SneakyThrows + public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + var builder = + RelyingPartyRegistrations.fromMetadataLocation(properties.getMetadataUrl()) + .registrationId(properties.getRegistrationId()) + .entityId(properties.getIssuerId()) + .assertionConsumerServiceLocation(properties.getAssertionConsumerServiceLocation()); + Saml2X509Credential decryptionCredential = properties.getDecryptionCredential(); + if (decryptionCredential != null) { + builder.decryptionX509Credentials(credentials -> credentials.add(decryptionCredential)); + } + RelyingPartyRegistration registration = builder.build(); + return new InMemoryRelyingPartyRegistrationRepository(registration); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + authConfig.configure(http); + var authenticationProvider = new OpenSaml4AuthenticationProvider(); + authenticationProvider.setResponseAuthenticationConverter(responseAuthenticationConverter()); + http.rememberMe(Customizer.withDefaults()) + .saml2Login( + saml -> + saml.authenticationManager(new ProviderManager(authenticationProvider)) + .loginProcessingUrl(properties.getLoginProcessingUrl()) + .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository())); + } + } +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java deleted file mode 100644 index 18763f8f6a..0000000000 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSSOConfig.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * Copyright 2023 Apple, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.netflix.spinnaker.gate.security.saml; - -import static org.springframework.security.extensions.saml2.config.SAMLConfigurer.saml; - -import com.netflix.spinnaker.gate.config.AuthConfig; -import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; -import java.net.InetAddress; -import javax.annotation.Nullable; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.opensaml.xml.security.BasicSecurityConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.saml.userdetails.SAMLUserDetailsService; -import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl; -import org.springframework.session.web.http.DefaultCookieSerializer; -import org.springframework.util.StringUtils; - -/** - * Configures SAML2 authentication for Spinnaker. - * - * @see SAML - * 2.0 configuration docs - */ -@Log4j2 -@ConditionalOnExpression("${saml.enabled:false}") -@Configuration -@SpinnakerAuthConfig -@EnableWebSecurity -@EnableConfigurationProperties(SAMLSecurityConfigProperties.class) -@ComponentScan -@RequiredArgsConstructor -public class SAMLSSOConfig extends WebSecurityConfigurerAdapter { - private final DefaultCookieSerializer defaultCookieSerializer; - private final AuthConfig authConfig; - private final SAMLUserDetailsService samlUserDetailsService; - private final SAMLSecurityConfigProperties samlSecurityConfigProperties; - @Nullable private final ServerProperties serverProperties; - - @Override - protected void configure(HttpSecurity http) throws Exception { - // We need our session cookie to come across when we get redirected back from the IdP: - defaultCookieSerializer.setSameSite(null); - authConfig.configure(http); - - http.rememberMe() - .key("password") - .rememberMeCookieName("cookieName") - .rememberMeParameter("rememberMe"); - - var webSSOProfileConsumer = new WebSSOProfileConsumerImpl(); - webSSOProfileConsumer.setMaxAuthenticationAge( - samlSecurityConfigProperties.getMaxAuthenticationAge()); - - var hostname = samlSecurityConfigProperties.getRedirectHostname(); - if (!StringUtils.hasLength(hostname) && serverProperties != null) { - InetAddress address = serverProperties.getAddress(); - if (address != null) { - hostname = address.getHostName(); - } - } - - // @formatter:off - - saml() - .userDetailsService(samlUserDetailsService) - .identityProvider() - .metadataFilePath(samlSecurityConfigProperties.getMetadataUrl()) - .discoveryEnabled(false) - .and() - .webSSOProfileConsumer(webSSOProfileConsumer) - .serviceProvider() - .entityId(samlSecurityConfigProperties.getIssuerId()) - .protocol(samlSecurityConfigProperties.getRedirectProtocol()) - .hostname(hostname) - .basePath(samlSecurityConfigProperties.getRedirectBasePath()) - .keyStore() - .storeFilePath(samlSecurityConfigProperties.getKeyStore()) - .password(samlSecurityConfigProperties.getKeyStorePassword()) - .keyname(samlSecurityConfigProperties.getKeyStoreAliasName()) - .keyPassword(samlSecurityConfigProperties.getKeyStorePassword()) - .and() - .and() - .init(http); - - // @formatter:on - - // Need to be after SAMLConfigurer initializes the global SecurityConfiguration - var secConfig = org.opensaml.Configuration.getGlobalSecurityConfiguration(); - if (secConfig instanceof BasicSecurityConfiguration) { - var config = (BasicSecurityConfiguration) secConfig; - var digest = samlSecurityConfigProperties.signatureDigest(); - log.info("Using {} digest for signing SAML messages", digest); - config.registerSignatureAlgorithmURI("RSA", digest.getSignatureMethod()); - config.setSignatureReferenceDigestMethod(digest.getDigestMethod()); - } else { - log.warn( - "Unable to find global BasicSecurityConfiguration (found '{}'). Ignoring signatureDigest configuration value.", - secConfig); - } - } -} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java deleted file mode 100644 index c187c3e5f9..0000000000 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLUserDetailsService.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2014 Netflix, Inc. - * Copyright 2023 Apple, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.netflix.spinnaker.gate.security.saml; - -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; -import com.netflix.spinnaker.gate.security.AllowedAccountsSupport; -import com.netflix.spinnaker.gate.services.PermissionService; -import com.netflix.spinnaker.kork.core.RetrySupport; -import com.netflix.spinnaker.kork.exceptions.ConfigurationException; -import com.netflix.spinnaker.security.User; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import java.time.Duration; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import lombok.extern.log4j.Log4j2; -import org.opensaml.saml2.core.Assertion; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.saml2.core.AttributeStatement; -import org.opensaml.xml.schema.XSAny; -import org.opensaml.xml.schema.XSString; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.saml.SAMLCredential; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -@Log4j2 -@Service -@ConditionalOnProperty("saml.enabled") -public class SAMLUserDetailsService - implements org.springframework.security.saml.userdetails.SAMLUserDetailsService { - private static final String COUNTER_NAME = "fiat.login"; - private static final Tag TYPE = Tag.of("type", "saml"); - private static final Tag SUCCESS = Tag.of("success", "true"); - private static final Tag FAILURE = Tag.of("success", "false"); - private static final Tag NO_FALLBACK = Tag.of("fallback", "none"); - - private final PermissionService permissionService; - private final AllowedAccountsSupport allowedAccountsSupport; - private final FiatClientConfigurationProperties fiatClientConfigurationProperties; - private final SAMLSecurityConfigProperties samlSecurityConfigProperties; - private final Counter successes; - private final Counter failures; - private final RetrySupport retrySupport = new RetrySupport(); - - public SAMLUserDetailsService( - PermissionService permissionService, - AllowedAccountsSupport allowedAccountsSupport, - FiatClientConfigurationProperties fiatClientConfigurationProperties, - SAMLSecurityConfigProperties samlSecurityConfigProperties, - MeterRegistry meterRegistry) { - this.permissionService = permissionService; - this.allowedAccountsSupport = allowedAccountsSupport; - this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; - this.samlSecurityConfigProperties = samlSecurityConfigProperties; - successes = meterRegistry.counter(COUNTER_NAME, Tags.of(TYPE, SUCCESS, NO_FALLBACK)); - failures = - meterRegistry.counter( - COUNTER_NAME, - Tags.of( - TYPE, - FAILURE, - Tag.of( - "fallback", - Boolean.toString(fiatClientConfigurationProperties.isLegacyFallback())))); - } - - @Override - public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - var assertion = credential.getAuthenticationAssertion(); - var attributes = extractAttributes(assertion); - var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); - @SuppressWarnings("deprecation") - var user = new User(); - - var subjectNameId = assertion.getSubject().getNameID().getValue(); - var emailAttributeValue = - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getEmail())); - var email = emailAttributeValue != null ? emailAttributeValue : subjectNameId; - user.setEmail(email); - var usernameAttributeValue = - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getUsername())); - var username = usernameAttributeValue != null ? usernameAttributeValue : subjectNameId; - user.setUsername(username); - var roles = extractRoles(attributes); - user.setRoles(roles); - - if (!CollectionUtils.isEmpty(samlSecurityConfigProperties.getRequiredRoles())) { - var requiredRoles = Set.copyOf(samlSecurityConfigProperties.getRequiredRoles()); - // check for at least one common role in both sets - if (Collections.disjoint(roles, requiredRoles)) { - throw new BadCredentialsException( - String.format("User %s is not in any required role from %s", email, requiredRoles)); - } - } - - Supplier login = - () -> { - permissionService.loginWithRoles(username, roles); - return null; - }; - - try { - retrySupport.retry(login, 5, Duration.ofSeconds(2), false); - log.debug( - "Successful SAML authentication (user: {}, roleCount: {}, roles: {})", - username, - roles.size(), - roles); - successes.increment(); - } catch (Exception e) { - boolean legacyFallback = fiatClientConfigurationProperties.isLegacyFallback(); - log.debug( - "Unsuccessful SAML authentication (user: {}, roleCount: {}, roles: {}, legacyFallback: {})", - username, - roles.size(), - roles, - legacyFallback, - e); - failures.increment(); - - if (!legacyFallback) { - throw e; - } - } - - user.setFirstName( - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getFirstName()))); - user.setLastName( - CollectionUtils.firstElement(attributes.get(userAttributeMapping.getLastName()))); - user.setAllowedAccounts(allowedAccountsSupport.filterAllowedAccounts(username, roles)); - - return user; - } - - private Set extractRoles(Map> attributes) { - var userAttributeMapping = samlSecurityConfigProperties.getUserAttributeMapping(); - var roleStream = - attributes.getOrDefault(userAttributeMapping.getRoles(), List.of()).stream() - .flatMap(roles -> Stream.of(roles.split(userAttributeMapping.getRolesDelimiter()))) - .map(SAMLUserDetailsService::parseRole); - if (samlSecurityConfigProperties.isForceLowercaseRoles()) { - roleStream = roleStream.map(String::toLowerCase); - } - if (samlSecurityConfigProperties.isSortRoles()) { - roleStream = roleStream.sorted(); - } - return roleStream.collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Map> extractAttributes(Assertion assertion) { - return assertion.getAttributeStatements().stream() - .flatMap(SAMLUserDetailsService::streamAttributes) - .collect( - Collectors.groupingBy( - Attribute::getName, - Collectors.flatMapping( - SAMLUserDetailsService::streamAttributeValues, Collectors.toList()))); - } - - private static Stream streamAttributes(AttributeStatement statement) { - return statement.getAttributes().stream(); - } - - private static Stream streamAttributeValues(Attribute attribute) { - return attribute.getAttributeValues().stream() - .map( - object -> { - if (object instanceof XSString) { - return ((XSString) object).getValue(); - } - if (object instanceof XSAny) { - return ((XSAny) object).getTextContent(); - } - return null; - }) - .filter(Objects::nonNull); - } - - private static String parseRole(String role) { - if (!role.contains("CN=")) { - return role; - } - try { - return new LdapName(role) - .getRdns().stream() - .filter(rdn -> rdn.getType().equals("CN")) - .map(rdn -> (String) rdn.getValue()) - .findFirst() - .orElseThrow( - () -> - new ConfigurationException( - String.format( - "SAML role '%s' contains 'CN=' but cannot be parsed as a DN", role))); - } catch (InvalidNameException e) { - throw new ConfigurationException( - String.format("Unable to parse SAML role name '%s'", role), e); - } - } -} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java similarity index 50% rename from gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java rename to gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java index 5644fb435a..288234b097 100644 --- a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigProperties.java +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java @@ -1,5 +1,4 @@ /* - * Copyright 2014 Netflix, Inc. * Copyright 2023 Apple, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,88 +12,116 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package com.netflix.spinnaker.gate.security.saml; +import com.netflix.spinnaker.kork.annotations.NullableByDefault; import com.netflix.spinnaker.kork.exceptions.ConfigurationException; import java.io.IOException; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.List; -import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nonnull; import javax.annotation.PostConstruct; -import javax.xml.crypto.dsig.DigestMethod; -import javax.xml.crypto.dsig.SignatureMethod; +import javax.validation.constraints.NotEmpty; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; -import org.opensaml.xml.signature.SignatureConstants; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; @Getter @Setter +@Validated @ConfigurationProperties("saml") -public class SAMLSecurityConfigProperties { - private static final String FILE_SCHEME = "file:"; - - private String keyStore; +@NullableByDefault +public class SecuritySamlProperties { + private Path keyStore; private String keyStoreType = "PKCS12"; private String keyStorePassword; - private String keyStoreAliasName; + private String keyStoreAliasName = "mykey"; // default alias for keytool + + public Saml2X509Credential getDecryptionCredential() + throws IOException, GeneralSecurityException { + if (keyStore == null) { + return null; + } + if (keyStoreType == null) { + keyStoreType = "PKCS12"; + } + KeyStore store = KeyStore.getInstance(keyStoreType); + char[] password = keyStorePassword != null ? keyStorePassword.toCharArray() : new char[0]; + try (var stream = Files.newInputStream(keyStore)) { + store.load(stream, password); + } + String alias = keyStoreAliasName; + var certificate = (X509Certificate) store.getCertificate(alias); + var privateKey = (PrivateKey) store.getKey(alias, password); + return Saml2X509Credential.decryption(privateKey, certificate); + } - // SAML DSL uses a metadata URL instead of hard coding a certificate/issuerId/redirectBase into - // the config. + /** URL pointing to the SAML metadata to use. */ private String metadataUrl; - // The parts of this endpoint passed to/used by the SAML IdP. - private String redirectProtocol = "https"; - private String redirectHostname; - private String redirectBasePath = "/"; - // The application identifier given to the IdP for this app. - private String issuerId; + /** Registration id for this SAML provider. Used in SAML processing URLs. */ + @NotEmpty private String registrationId = "SSO"; + + /** + * The Relying Party's entity ID (sometimes called an issuer ID). The value may contain a number + * of placeholders. They are "baseUrl", "registrationId", "baseScheme", "baseHost", and + * "basePort". + */ + @NotEmpty private String issuerId = "{baseUrl}/saml2/metadata"; + + /** + * The path used for login processing. When combined with the base URL, this should form the + * assertion consumer service location. + */ + @NotEmpty private String loginProcessingUrl = "/saml/{registrationId}"; + + /** + * Returns the assertion consumer service location template to use for redirecting back from the + * identity provider. + */ + public String getAssertionConsumerServiceLocation() { + return "{baseUrl}" + loginProcessingUrl; + } + + /** Optional list of roles required for authentication to succeed. */ private List requiredRoles; + + /** Determines whether to sort the roles returned from the SAML provider. */ private boolean sortRoles = false; + + /** Toggles whether role names should be converted to lowercase. */ private boolean forceLowercaseRoles = true; - @NestedConfigurationProperty + @Nonnull @NestedConfigurationProperty private UserAttributeMapping userAttributeMapping = new UserAttributeMapping(); - private long maxAuthenticationAge = 7200; - - // SHA1 is the default registered in DefaultSecurityConfigurationBootstrap.populateSignatureParams - private String signatureDigest = "SHA1"; - - public SignatureDigest signatureDigest() { - return SignatureDigest.fromName(signatureDigest); - } - - /** - * Ensure that the keystore exists and can be accessed with the given keyStorePassword and - * keyStoreAliasName. Validates the configured signature/digest is supported. - */ @PostConstruct public void validate() throws IOException, GeneralSecurityException { if (StringUtils.hasLength(metadataUrl) && metadataUrl.startsWith("/")) { - metadataUrl = FILE_SCHEME + metadataUrl; + metadataUrl = "file:" + metadataUrl; } - if (StringUtils.hasLength(keyStore)) { - if (!keyStore.startsWith(FILE_SCHEME)) { - keyStore = FILE_SCHEME + keyStore; + if (keyStore != null) { + if (keyStoreType == null) { + keyStoreType = "PKCS12"; } - var path = Path.of(URI.create(keyStore)); var keystore = KeyStore.getInstance(keyStoreType); var password = keyStorePassword != null ? keyStorePassword.toCharArray() : new char[0]; - try (var stream = Files.newInputStream(path)) { + try (var stream = Files.newInputStream(keyStore)) { // will throw an exception if `keyStorePassword` is invalid or if the key store file is // invalid keystore.load(stream, password); @@ -109,12 +136,11 @@ public void validate() throws IOException, GeneralSecurityException { } } } - // Validate signature digest algorithm - Objects.requireNonNull(signatureDigest()); } + @Nonnull private static Set caseInsensitiveSetFromAliasEnumeration( - Enumeration enumeration) { + @Nonnull Enumeration enumeration) { Set set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); while (enumeration.hasMoreElements()) { set.add(enumeration.nextElement()); @@ -124,36 +150,13 @@ private static Set caseInsensitiveSetFromAliasEnumeration( @Getter @Setter + @Validated public static class UserAttributeMapping { - private String firstName = "User.FirstName"; - private String lastName = "User.LastName"; - private String roles = "memberOf"; - private String rolesDelimiter = ";"; + @NotEmpty private String firstName = "User.FirstName"; + @NotEmpty private String lastName = "User.LastName"; + @NotEmpty private String roles = "memberOf"; + @NotEmpty private String rolesDelimiter = ";"; private String username; private String email; } - - // only RSA-based signatures explicitly supported here (baseline requirement for XML signatures) - @Getter - @RequiredArgsConstructor - public enum SignatureDigest { - @Deprecated - SHA1(SignatureMethod.RSA_SHA1, DigestMethod.SHA1), - SHA256(SignatureMethod.RSA_SHA256, DigestMethod.SHA256), - SHA384(SignatureMethod.RSA_SHA384, DigestMethod.SHA384), - SHA512(SignatureMethod.RSA_SHA512, DigestMethod.SHA512), - @Deprecated - RIPEMD160(SignatureConstants.ALGO_ID_SIGNATURE_RSA_RIPEMD160, DigestMethod.RIPEMD160), - @Deprecated - MD5( - SignatureConstants.ALGO_ID_SIGNATURE_NOT_RECOMMENDED_RSA_MD5, - SignatureConstants.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5), - ; - private final String signatureMethod; - private final String digestMethod; - - public static SignatureDigest fromName(String name) { - return valueOf(name.toUpperCase(Locale.ROOT)); - } - } } diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java new file mode 100644 index 0000000000..c34f324675 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserIdentifierExtractor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** Strategy for extracting a userid from an authenticated SAML2 principal. */ +public interface UserIdentifierExtractor { + String fromPrincipal(Saml2AuthenticatedPrincipal principal); +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java new file mode 100644 index 0000000000..1eee3bef9c --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/UserRolesExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.gate.security.saml; + +import java.util.Set; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; + +/** Strategy for extracting and potentially filtering roles from a SAML assertion. */ +public interface UserRolesExtractor { + /** Returns the roles to assign the given principal. */ + Set getRoles(Saml2AuthenticatedPrincipal principal); +} diff --git a/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java new file mode 100644 index 0000000000..4b8309fff5 --- /dev/null +++ b/gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Apple, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +@NonnullByDefault +package com.netflix.spinnaker.gate.security.saml; + +import com.netflix.spinnaker.kork.annotations.NonnullByDefault; diff --git a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy b/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy deleted file mode 100644 index 7a5f7cfc53..0000000000 --- a/gate-saml/src/test/groovy/com/netflix/spinnaker/gate/security/saml/SAMLSecurityConfigPropertiesSpec.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.netflix.spinnaker.gate.security.saml - -import spock.lang.Specification -import spock.lang.Unroll - -class SAMLSecurityConfigPropertiesSpec extends Specification { - @Unroll - def "should validate that the keystore exists and the password/alias are valid"() { - given: - def ssoConfig = new SAMLSecurityConfigProperties( - keyStore: keyStore.toString(), keyStorePassword: keyStorePassword, keyStoreAliasName: keyStoreAliasName - ) - - expect: - try { - ssoConfig.validate() - assert !expectsException - } catch (Exception ignored) { - assert expectsException - } - - try { - // ensure validation works if a keystore is not prefixed with "file:" - ssoConfig.keyStore = ssoConfig ? ssoConfig.keyStore.replaceAll("file:", "") : null - ssoConfig.validate() - assert !expectsException - } catch (Exception ignored) { - assert expectsException - } - - where: - keyStore | keyStorePassword | keyStoreAliasName || expectsException - this.class.getResource("/does-not-exist.jks") | null | null || true // keystore does not exist - this.class.getResource("/saml-client.jks") | "invalid" | "saml-client" || true // password is invalid - this.class.getResource("/saml-client.jks") | "123456" | "invalid" || true // alias is invalid - this.class.getResource("/saml-client.jks") | "123456" | "saml-client" || false - } -} diff --git a/gate-saml/src/test/resources/saml-client.jks b/gate-saml/src/test/resources/saml-client.jks deleted file mode 100644 index ea2a99098d129cc904a9f5603fb85332d95d3f72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1300 zcmezO_TO6u1_mY|W&~sI;>6q>-Q=9i)Vva)SR}*cmt{b@-=K-H+klUaOPh_6g;9%1 zkdcvvj9EL3`xSkbg3{DE!XLvclqv#Ts+PnfMy z+VJdgss8Cct6alh(Vml(Le)D@wI;=X6JAgtk+t(zs@c{z>%Hr5@BZKWM3Hg2wR6D> zGpXn+W>THq;*Nl$$Jry>AM+g4S{uAS=Ejrhn-cvZA0XC;JYNlm1~H{`}aA zWup8nzni~ZJ}P&Z$uvYbN^GLhx6;q+v=gsuz5k@dEIIe02jg)jQHJRW0sW%N+-EjT z+S#<|bqn{02$`=Sr+t68p!M2p+k2%KF{EtX?(!4EN!-Wp7Fkj12GF zbtTgOSs}YXW5X6tu|nk+J{n@e+s|_v^_*Dt&_=APJEW#pQ0Mms18+4Vf3;7civNrxv`Z2dSj8>#QeyB65H2ZMJ+UXW4&tPu_LTz|zAlN7AOqTTWl2#ZkDgP|qXX9w zFU8I0zgt{9#i^6oc=F7>^i0{MPCeGw)w4yM4=z?TkT;M8rdwG)7BLo)i>KyJn(5D9 zmt(fQRQ1J{qu0f+u0l!Y%=!!lI!q$2=k-nhI+-PDSo?Oq7SuXX?xF6$BvQIb?AGDW o|K9y{HDn8(;P6p#R^G~;jl6liG8eZw>w2Gmyrc1pv`F+F0I)*|)&Kwi