From fe51eda30a50ae79e1b8ce60eee14fda34a27148 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Sun, 9 Mar 2025 14:28:14 +0530 Subject: [PATCH 01/16] feature(OAuth2): Remove deprecated OAuth2 annotation. Instead Use Java DSL way of OAuth2 Oauth configuration properties are changed for new java way of dsl. Old Config Properties: security: authn: oauth2: enabled: true client: clientId: clientSecret: accessTokenUri: https://www.googleapis.com/oauth2/v4/token userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth scope: profile email userInfoRequirements: hd: resource: userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo userInfoMapping: email: email firstName: given_name lastName: family_name provider: GOOGLE New Config Properties: Google: spring: security: oauth2: client: registration: google: client-id: client-secret: authorization-grant-type: authorization_code redirect-uri: "https:///login/oauth2/code/google" scope: profile,email,openid client-name: google provider: google: authorization-uri: https://accounts.google.com/o/oauth2/auth token-uri: https://oauth2.googleapis.com/token user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo user-name-attribute: sub Github: spring: security: oauth2: client: registration: userInfoMapping: email: email firstName: '' lastName: name username: login github: client-id: client-secret: authorization-grant-type: authorization_code redirect-uri: "https:///login/oauth2/code/github" scope: user,email client-name: github provider: github: authorization-uri: https://github.com/login/oauth/authorize token-uri: https://github.com/login/oauth/access_token user-info-uri: https://api.github.com/user user-name-attribute: login --- gate-oauth2/gate-oauth2.gradle | 2 +- .../oauth2/ExternalAuthTokenFilter.java | 71 ------- .../gate/security/oauth2/OAuth2SsoConfig.java | 122 ++--------- .../security/oauth2/OAuthConfigEnabled.java | 55 +++++ ...s.java => OAuthUserInfoServiceHelper.java} | 184 +++++++---------- .../security/oauth2/SpinnakerOAuth2User.java | 61 ++++++ .../SpinnakerOAuth2UserInfoService.java | 36 ++++ .../security/oauth2/SpinnakerOIDCUser.java | 84 ++++++++ .../oauth2/SpinnakerOIDCUserInfoService.java | 36 ++++ .../provider/GithubProviderTokenServices.java | 106 ---------- .../oauth2/ExternalAuthTokenFilterTest.java | 82 -------- .../oauth2/OAuthConfigEnabledTest.java | 120 +++++++++++ .../OAuthUserInfoServiceHelperTest.java | 185 +++++++++++++++++ .../SpinnakerUserInfoTokenServicesTest.java | 195 ------------------ .../gate/security/oauth/OAuth2Test.java | 4 +- 15 files changed, 678 insertions(+), 665 deletions(-) delete mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilter.java create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java rename gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/{SpinnakerUserInfoTokenServices.java => OAuthUserInfoServiceHelper.java} (63%) create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java delete mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java delete mode 100644 gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilterTest.java create mode 100644 gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabledTest.java create mode 100644 gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java delete mode 100644 gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServicesTest.java diff --git a/gate-oauth2/gate-oauth2.gradle b/gate-oauth2/gate-oauth2.gradle index a6b0ccfcc4..258fba62e8 100644 --- a/gate-oauth2/gate-oauth2.gradle +++ b/gate-oauth2/gate-oauth2.gradle @@ -7,7 +7,7 @@ dependencies { implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-security" implementation "org.apache.groovy:groovy-json" - implementation "org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure" + implementation "org.springframework.boot:spring-boot-starter-oauth2-client" implementation "org.springframework.session:spring-session-core" testImplementation "com.squareup.retrofit2:retrofit-mock" diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilter.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilter.java deleted file mode 100644 index dc1bf54e41..0000000000 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 OpsMx, 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.oauth2; - -import java.io.IOException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor; -import org.springframework.stereotype.Component; - -/** - * This class supports the use case of an externally provided OAuth access token, for example, a - * Github-issued personal access token. - */ -@Component -public class ExternalAuthTokenFilter implements Filter { - - @Autowired(required = false) - private UserInfoRestTemplateFactory userInfoRestTemplateFactory; - - private BearerTokenExtractor extractor = new BearerTokenExtractor(); - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - Authentication auth = extractor.extract(httpServletRequest); - if (auth != null && auth.getPrincipal() != null && !auth.getPrincipal().toString().isEmpty()) { - DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(auth.getPrincipal().toString()); - // Reassign token type to be capitalized "Bearer", - // see https://github.com/spinnaker/spinnaker/issues/2074 - token.setTokenType(OAuth2AccessToken.BEARER_TYPE); - if (userInfoRestTemplateFactory != null) { - OAuth2ClientContext ctx = - userInfoRestTemplateFactory.getUserInfoRestTemplate().getOAuth2ClientContext(); - ctx.setAccessToken(token); - } - } - chain.doFilter(request, response); - } - - @Override - public void init(FilterConfig filterConfig) {} - - @Override - public void destroy() {} -} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java index 159b7b67da..b7bbbf49d5 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java @@ -15,102 +15,46 @@ */ package com.netflix.spinnaker.gate.security.oauth2; -import com.netflix.spectator.api.Registry; -import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; import com.netflix.spinnaker.gate.config.AuthConfig; -import com.netflix.spinnaker.gate.security.AllowedAccountsSupport; import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig; -import com.netflix.spinnaker.gate.security.oauth2.provider.SpinnakerProviderTokenServices; -import com.netflix.spinnaker.gate.services.CredentialsService; -import com.netflix.spinnaker.gate.services.PermissionService; -import com.netflix.spinnaker.gate.services.internal.Front50Service; import java.util.HashMap; -import java.util.Optional; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import lombok.Data; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2SsoProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; 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.core.AuthenticationException; -import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; -import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; -import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.stereotype.Component; @Configuration -@SpinnakerAuthConfig @EnableWebSecurity -@EnableOAuth2Sso -@EnableConfigurationProperties -@ConditionalOnProperty(name = "security.oauth2.client.clientId") -@Slf4j +@SpinnakerAuthConfig +@Conditional(OAuthConfigEnabled.class) public class OAuth2SsoConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthConfig authConfig; - @Autowired private ExternalAuthTokenFilter externalAuthTokenFilter; - @Autowired private ExternalSslAwareEntryPoint entryPoint; + @Autowired private SpinnakerOAuth2UserInfoService customOAuth2UserService; + @Autowired private SpinnakerOIDCUserInfoService oidcUserInfoService; @Autowired private DefaultCookieSerializer defaultCookieSerializer; - - @Primary - @Bean - @ConditionalOnProperty( - prefix = "security.oauth2.resource.spinnaker-user-info-token-services", - name = "enabled", - havingValue = "true", - matchIfMissing = true) - public ResourceServerTokenServices spinnakerUserInfoTokenServices( - ResourceServerProperties sso, - UserInfoTokenServices userInfoTokenServices, - CredentialsService credentialsService, - OAuth2SsoConfig.UserInfoMapping userInfoMapping, - OAuth2SsoConfig.UserInfoRequirements userInfoRequirements, - PermissionService permissionService, - Front50Service front50Service, - Optional providerTokenServices, - AllowedAccountsSupport allowedAccountsSupport, - FiatClientConfigurationProperties fiatClientConfigurationProperties, - Registry registry) { - return new SpinnakerUserInfoTokenServices( - sso, - userInfoTokenServices, - credentialsService, - userInfoMapping, - userInfoRequirements, - permissionService, - front50Service, - providerTokenServices, - allowedAccountsSupport, - fiatClientConfigurationProperties, - registry); - } + @Autowired private ClientRegistrationRepository clientRegistrationRepository; @Override - public void configure(HttpSecurity http) throws Exception { + public void configure(HttpSecurity httpSecurity) throws Exception { defaultCookieSerializer.setSameSite(null); - authConfig.configure(http); - - http.exceptionHandling().authenticationEntryPoint(entryPoint); - http.addFilterBefore( - new BasicAuthenticationFilter(authenticationManager()), - UsernamePasswordAuthenticationFilter.class); - http.addFilterBefore(externalAuthTokenFilter, AbstractPreAuthenticatedProcessingFilter.class); + authConfig.configure(httpSecurity); + httpSecurity + .authorizeRequests(auth -> auth.anyRequest().authenticated()) + .oauth2Login( + oauth2 -> + oauth2.userInfoEndpoint( + userInfo -> + userInfo + .userService(customOAuth2UserService) + .oidcUserService(oidcUserInfoService))); } /** @@ -118,7 +62,7 @@ public void configure(HttpSecurity http) throws Exception { * be in the User. */ @Component - @ConfigurationProperties("security.oauth2.user-info-mapping") + @ConfigurationProperties("spring.security.oauth2.client.registration.user-info-mapping") @Data public static class UserInfoMapping { private String email = "email"; @@ -130,32 +74,6 @@ public static class UserInfoMapping { } @Component - @ConfigurationProperties("security.oauth2.user-info-requirements") + @ConfigurationProperties("spring.security.oauth2.client.registration.user-info-requirements") public static class UserInfoRequirements extends HashMap {} - - /** - * This class exists to change the login redirect (to /login) to the same URL as the - * preEstablishedRedirectUri, if set, where the SSL is terminated outside of this server. - */ - @Component - @ConditionalOnProperty(name = "security.oauth2.client.client-id") - public static class ExternalSslAwareEntryPoint extends LoginUrlAuthenticationEntryPoint { - @Autowired private AuthorizationCodeResourceDetails details; - - @Autowired - public ExternalSslAwareEntryPoint(OAuth2SsoProperties sso) { - super(sso.getLoginPath()); - } - - @Override - protected String determineUrlToUseForThisRequest( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) { - final String uri = details.getPreEstablishedRedirectUri(); - return uri != null - ? uri - : super.determineUrlToUseForThisRequest(request, response, exception); - } - } } diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java new file mode 100644 index 0000000000..77a073e713 --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.type.AnnotatedTypeMetadata; + +@Slf4j +public class OAuthConfigEnabled implements Condition { + private static final String SPRING_SECURITY_OAUTH2_REGEX = + "spring\\.security\\.oauth2\\.client\\.registration\\..*\\.client-id"; + private static final Pattern SPRING_SECURITY_OAUTH2_PATTERN = + Pattern.compile(SPRING_SECURITY_OAUTH2_REGEX); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (!(context.getEnvironment() instanceof ConfigurableEnvironment)) { + return false; + } + + ConfigurableEnvironment env = (ConfigurableEnvironment) context.getEnvironment(); + + for (PropertySource propertySource : env.getPropertySources()) { + if (propertySource instanceof EnumerablePropertySource) { + for (String propertyName : + ((EnumerablePropertySource) propertySource).getPropertyNames()) { + if (SPRING_SECURITY_OAUTH2_PATTERN.matcher(propertyName).matches()) { + return true; // If any property matches, load the configuration + } + } + } + } + return false; // Skip configuration if no matching properties found + } +} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServices.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java similarity index 63% rename from gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServices.java rename to gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index 5fc4bd07ad..c9f642acee 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServices.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -1,7 +1,7 @@ /* * Copyright 2025 OpsMx, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.netflix.spinnaker.gate.security.oauth2; import static net.logstash.logback.argument.StructuredArguments.entries; @@ -23,13 +24,11 @@ import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; import com.netflix.spinnaker.gate.security.AllowedAccountsSupport; import com.netflix.spinnaker.gate.security.oauth2.provider.SpinnakerProviderTokenServices; -import com.netflix.spinnaker.gate.services.CredentialsService; import com.netflix.spinnaker.gate.services.PermissionService; import com.netflix.spinnaker.gate.services.internal.Front50Service; import com.netflix.spinnaker.kork.core.RetrySupport; import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException; -import com.netflix.spinnaker.security.User; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -45,90 +44,57 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.OAuth2Request; -import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; -import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; /** - * ResourceServerTokenServices is an interface used to manage access tokens. The - * UserInfoTokenService object is an implementation of that interface that uses an access token to - * get the logged in user's data (such as email or profile). We want to customize the Authentication - * object that is returned to include our custom (Kork) User. + * A helper class to handle common user loading logic for both OAuth2 and OIDC authentication. This + * class extracts shared code to avoid duplication in OAuth2 and OIDC user loading processes. */ +@Component @Slf4j -public class SpinnakerUserInfoTokenServices implements ResourceServerTokenServices { +public class OAuthUserInfoServiceHelper { + + @Autowired private OAuth2SsoConfig.UserInfoMapping userInfoMapping; + + @Autowired private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; - private final ResourceServerProperties sso; - private final UserInfoTokenServices userInfoTokenServices; - private final CredentialsService credentialsService; - private final OAuth2SsoConfig.UserInfoMapping userInfoMapping; - private final OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; - private final PermissionService permissionService; - private final Front50Service front50Service; - private final SpinnakerProviderTokenServices providerTokenServices; + @Autowired private PermissionService permissionService; - private final AllowedAccountsSupport allowedAccountsSupport; - private final FiatClientConfigurationProperties fiatClientConfigurationProperties; - private final Registry registry; + @Autowired private Front50Service front50Service; + + @Autowired(required = false) + private SpinnakerProviderTokenServices providerTokenServices; + + @Autowired private AllowedAccountsSupport allowedAccountsSupport; + + @Autowired private FiatClientConfigurationProperties fiatClientConfigurationProperties; + + @Autowired private Registry registry; @Autowired(required = false) @Qualifier("spinnaker-oauth2-group-extractor") - private BiFunction> groupExtractor; - - private RetrySupport retrySupport = new RetrySupport(); - - @Autowired - public SpinnakerUserInfoTokenServices( - ResourceServerProperties sso, - UserInfoTokenServices userInfoTokenServices, - CredentialsService credentialsService, - OAuth2SsoConfig.UserInfoMapping userInfoMapping, - OAuth2SsoConfig.UserInfoRequirements userInfoRequirements, - PermissionService permissionService, - Front50Service front50Service, - Optional providerTokenServices, - AllowedAccountsSupport allowedAccountsSupport, - FiatClientConfigurationProperties fiatClientConfigurationProperties, - Registry registry) { - this.sso = sso; - this.userInfoTokenServices = userInfoTokenServices; - this.credentialsService = credentialsService; - this.userInfoMapping = userInfoMapping; - this.userInfoRequirements = userInfoRequirements; - this.permissionService = permissionService; - this.front50Service = front50Service; - this.providerTokenServices = providerTokenServices.orElse(null); - this.allowedAccountsSupport = allowedAccountsSupport; - this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; - this.registry = registry; - } + private BiFunction, List> groupExtractor; - @Override - public OAuth2Authentication loadAuthentication(final String accessToken) - throws AuthenticationException, InvalidTokenException { - OAuth2Authentication oAuth2Authentication = - userInfoTokenServices.loadAuthentication(accessToken); + private final RetrySupport retrySupport = new RetrySupport(); - final Map details = - (Map) oAuth2Authentication.getUserAuthentication().getDetails(); + T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest userRequest) { + Map details = oAuth2User.getAttributes(); if (log.isDebugEnabled()) { log.debug("UserInfo details: " + entries(details)); } boolean isServiceAccount = isServiceAccount(details); + String accessToken = userRequest.getAccessToken().getTokenValue(); + if (!isServiceAccount) { if (!hasAllUserInfoRequirements(details)) { throw new BadCredentialsException("User's info does not have all required fields."); } - if (providerTokenServices != null && !providerTokenServices.hasAllProviderRequirements(accessToken, details)) { throw new BadCredentialsException( @@ -185,45 +151,38 @@ public OAuth2Authentication loadAuthentication(final String accessToken) } } - User spinnakerUser = new User(); - spinnakerUser.setEmail(toStringOrNull(details.get(userInfoMapping.getEmail()))); - spinnakerUser.setFirstName(toStringOrNull(details.get(userInfoMapping.getFirstName()))); - spinnakerUser.setLastName(toStringOrNull(details.get(userInfoMapping.getLastName()))); - spinnakerUser.setAllowedAccounts(allowedAccountsSupport.filterAllowedAccounts(username, roles)); - spinnakerUser.setRoles(roles); - spinnakerUser.setUsername(username); - - PreAuthenticatedAuthenticationToken authentication = - new PreAuthenticatedAuthenticationToken( - spinnakerUser, null, spinnakerUser.getAuthorities()); - - // impl copied from UserInfoTokenServices - OAuth2Request storedRequest = - new OAuth2Request(null, sso.getClientId(), null, true, null, null, null, null, null); - - return new OAuth2Authentication(storedRequest, authentication); - } - - /** - * Safely converts an object to a string representation. - * - *

This method checks if the provided object is non-null before calling {@code toString()}. If - * the object is {@code null}, it returns {@code null} instead of throwing a {@code - * NullPointerException}. - * - * @param o the object to convert to a string, may be {@code null} - * @return the string representation of the object, or {@code null} if the object is {@code null} - */ - private String toStringOrNull(Object o) { - return o != null ? o.toString() : null; - } - - @Override - public OAuth2AccessToken readAccessToken(String accessToken) { - return userInfoTokenServices.readAccessToken(accessToken); + if (oAuth2User instanceof OidcUser oidcUser) { + SpinnakerOIDCUser spinnakerUser = + new SpinnakerOIDCUser( + toStringOrNull(details.get(userInfoMapping.getEmail())), + toStringOrNull(details.get(userInfoMapping.getFirstName())), + toStringOrNull(details.get(userInfoMapping.getLastName())), + allowedAccountsSupport.filterAllowedAccounts(username, roles), + roles, + username, + oidcUser.getIdToken(), + oidcUser.getUserInfo()); + spinnakerUser.getAttributes().putAll(details); + spinnakerUser.getAuthorities().addAll(oAuth2User.getAuthorities()); + + return (T) spinnakerUser; + } else { + SpinnakerOAuth2User spinnakerUser = + new SpinnakerOAuth2User( + toStringOrNull(details.get(userInfoMapping.getEmail())), + toStringOrNull(details.get(userInfoMapping.getFirstName())), + toStringOrNull(details.get(userInfoMapping.getLastName())), + allowedAccountsSupport.filterAllowedAccounts(username, roles), + roles, + username); + spinnakerUser.getAttributes().putAll(details); + spinnakerUser.getAuthorities().addAll(oAuth2User.getAuthorities()); + + return (T) spinnakerUser; + } } - protected boolean isServiceAccount(Map details) { + boolean isServiceAccount(Map details) { String email = (String) details.get(userInfoMapping.getServiceAccountEmail()); if (email == null || !permissionService.isEnabled()) { return false; @@ -251,7 +210,7 @@ private static boolean valueMatchesConstraint(Object value, String requiredVal) return value.equals(requiredVal); } - public boolean hasAllUserInfoRequirements(Map details) { + boolean hasAllUserInfoRequirements(Map details) { if (userInfoRequirements == null || userInfoRequirements.isEmpty()) { return true; } @@ -290,7 +249,7 @@ public boolean hasAllUserInfoRequirements(Map details) { return invalidFields.isEmpty(); } - public static boolean isRegexExpression(String val) { + private static boolean isRegexExpression(String val) { if (val.startsWith("/") && val.endsWith("/")) { try { Pattern.compile(val); @@ -303,12 +262,12 @@ public static boolean isRegexExpression(String val) { return false; } - public static String mutateRegexPattern(String val) { + private static String mutateRegexPattern(String val) { // "/expr/" -> "expr" return val.substring(1, val.length() - 1); } - protected List getRoles(Map details) { + List getRoles(Map details) { if (userInfoMapping == null || userInfoMapping.getRoles() == null) { return List.of(); } @@ -344,4 +303,17 @@ private List parseJsonRoles(String jsonString) { return List.of(); } } + /** + * Safely converts an object to a string representation. + * + *

This method checks if the provided object is non-null before calling {@code toString()}. If + * the object is {@code null}, it returns {@code null} instead of throwing a {@code + * NullPointerException}. + * + * @param o the object to convert to a string, may be {@code null} + * @return the string representation of the object, or {@code null} if the object is {@code null} + */ + static String toStringOrNull(Object o) { + return o != null ? o.toString() : null; + } } diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java new file mode 100644 index 0000000000..0ddaefecbd --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import com.netflix.spinnaker.security.User; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +public class SpinnakerOAuth2User extends User implements OAuth2User { + private Map attribute = new HashMap<>(); + private List authorities = new ArrayList<>(); + + public SpinnakerOAuth2User( + String email, + String firstName, + String lastName, + Collection allowedAccounts, + List roles, + String username) { + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.allowedAccounts = allowedAccounts; + this.roles = roles; + this.username = username; + } + + @Override + public Map getAttributes() { + return attribute; + } + + @Override + public List getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return super.username; + } +} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java new file mode 100644 index 0000000000..a182416334 --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class SpinnakerOAuth2UserInfoService extends DefaultOAuth2UserService { + @Autowired private OAuthUserInfoServiceHelper userInfoService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + OAuth2User oAuth2User = super.loadUser(userRequest); + return userInfoService.getOAuthSpinnakerUser(oAuth2User, userRequest); + } +} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java new file mode 100644 index 0000000000..ca7e6f96ec --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import com.netflix.spinnaker.security.User; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +public class SpinnakerOIDCUser extends User implements OidcUser { + private Map attribute = new HashMap<>(); + private List authorities = new ArrayList<>(); + private final OidcIdToken idToken; + private final OidcUserInfo userInfo; + + public SpinnakerOIDCUser( + String email, + String firstName, + String lastName, + Collection allowedAccounts, + List roles, + String username, + OidcIdToken idToken, + OidcUserInfo userInfo) { + this.idToken = idToken; + this.userInfo = userInfo; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.allowedAccounts = allowedAccounts; + this.roles = roles; + this.username = username; + } + + @Override + public Map getAttributes() { + return attribute; + } + + @Override + public List getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return super.username; + } + + @Override + public Map getClaims() { + return this.attribute; + } + + @Override + public OidcUserInfo getUserInfo() { + return this.userInfo; + } + + @Override + public OidcIdToken getIdToken() { + return this.idToken; + } +} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java new file mode 100644 index 0000000000..2442c9a4c0 --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class SpinnakerOIDCUserInfoService extends OidcUserService { + @Autowired private OAuthUserInfoServiceHelper userInfoService; + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) { + OidcUser oidcUser = super.loadUser(userRequest); + return userInfoService.getOAuthSpinnakerUser(oidcUser, userRequest); + } +} diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java deleted file mode 100644 index fcde8ae2de..0000000000 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2025 OpsMx, 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.oauth2.provider; - -import java.util.List; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.security.oauth2.client.OAuth2RestOperations; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@ConditionalOnProperty(name = "security.oauth2.provider-requirements.type", havingValue = "github") -public class GithubProviderTokenServices implements SpinnakerProviderTokenServices { - - @Autowired private ResourceServerProperties sso; - @Autowired private GithubRequirements requirements; - private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; - private OAuth2RestOperations restTemplate; - - private boolean githubOrganizationMember( - String organization, List> organizations) { - for (int i = 0; i < organizations.size(); i++) { - if (organization.equals(organizations.get(i).get("login"))) { - return true; - } - } - return false; - } - - private boolean checkOrganization( - String accessToken, String organizationsUrl, String organization) { - try { - log.debug("Getting user organizations from URL {}", organizationsUrl); - OAuth2RestOperations restTemplate = this.restTemplate; - if (restTemplate == null) { - BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); - resource.setClientId(sso.getClientId()); - restTemplate = new OAuth2RestTemplate(resource); - } - - OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext().getAccessToken(); - if (existingToken == null || !accessToken.equals(existingToken.getValue())) { - DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken); - token.setTokenType(this.tokenType); - restTemplate.getOAuth2ClientContext().setAccessToken(token); - } - - List> organizations = - restTemplate.getForEntity(organizationsUrl, List.class).getBody(); - return githubOrganizationMember(organization, organizations); - } catch (Exception e) { - log.warn("Could not fetch user organizations", e); - return false; - } - } - - public boolean hasAllProviderRequirements(String token, Map details) { - boolean hasRequirements = true; - if (requirements.getOrganization() != null && details.containsKey("organizations_url")) { - boolean orgMatch = - checkOrganization( - token, (String) details.get("organizations_url"), requirements.getOrganization()); - if (!orgMatch) { - log.debug("User does not include required organization {}", requirements.getOrganization()); - hasRequirements = false; - } - } - return hasRequirements; - } - - @Component - @ConfigurationProperties("security.oauth2.provider-requirements") - public static class GithubRequirements { - public String getOrganization() { - return organization; - } - - public void setOrganization(String organization) { - this.organization = organization; - } - - private String organization; - } -} diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilterTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilterTest.java deleted file mode 100644 index f563ff7e40..0000000000 --- a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/ExternalAuthTokenFilterTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2025 OpsMx, 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.oauth2; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import javax.servlet.FilterChain; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.common.OAuth2AccessToken; - -public class ExternalAuthTokenFilterTest { - - @Mock private UserInfoRestTemplateFactory restTemplateFactory; - - @Mock private OAuth2RestTemplate restTemplate; - - @Mock private OAuth2ClientContext oauth2ClientContext; - - @InjectMocks private ExternalAuthTokenFilter filter; - - private MockHttpServletRequest request; - private MockHttpServletResponse response; - private FilterChain chain; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - request = new MockHttpServletRequest(); - response = new MockHttpServletResponse(); - chain = mock(FilterChain.class); - } - - @Test - void shouldEnsureBearerTokenIsForwardedProperly() - throws javax.servlet.ServletException, IOException { - // Arrange - request.addHeader("Authorization", "bearer foo"); - - OAuth2AccessToken token = mock(OAuth2AccessToken.class); - when(token.getTokenType()).thenReturn("Bearer"); - when(token.getValue()).thenReturn("foo"); - - when(restTemplateFactory.getUserInfoRestTemplate()).thenReturn(restTemplate); - when(restTemplate.getOAuth2ClientContext()).thenReturn(oauth2ClientContext); - when(oauth2ClientContext.getAccessToken()).thenReturn(token); - - // Act - filter.doFilter(request, response, chain); - - // Assert - verify(chain).doFilter(request, response); - assertThat(token.getTokenType()).isEqualTo("Bearer"); - assertThat(token.getValue()).isEqualTo("foo"); - } -} diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabledTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabledTest.java new file mode 100644 index 0000000000..c9f76ec164 --- /dev/null +++ b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabledTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.mock.env.MockPropertySource; + +class OAuthConfigEnabledTest { + + private OAuthConfigEnabled condition; + + @Mock private ConfigurableEnvironment environment; + @Mock private AnnotatedTypeMetadata metadata; + @Mock private ConditionContext context; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + condition = new OAuthConfigEnabled(); + when(context.getEnvironment()).thenReturn(environment); + } + + @Test + void testMatches_WhenOAuth2ConfigExists_ShouldReturnTrue() { + MockPropertySource propertySource = new MockPropertySource(); + propertySource.setProperty( + "spring.security.oauth2.client.registration.test-client.client-id", "test-client-id"); + + when(environment.getPropertySources()).thenReturn(new MockPropertySources(propertySource)); + + boolean result = condition.matches(context, metadata); + assertThat(result).isTrue(); + } + + @Test + void testMatches_WhenNoOAuth2Config_ShouldReturnFalse() { + MockPropertySource propertySource = new MockPropertySource(); + propertySource.setProperty("some.other.property", "value"); + when(environment.getPropertySources()).thenReturn(new MockPropertySources(propertySource)); + + boolean result = condition.matches(context, metadata); + assertThat(result).isFalse(); + } + + @Test + void testMatches_WhenEnvironmentNotConfigurable_ShouldReturnFalse() { + var nonConfigurableEnv = mock(org.springframework.core.env.Environment.class); + when(context.getEnvironment()).thenReturn(nonConfigurableEnv); + + boolean result = condition.matches(context, metadata); + assertThat(result).isFalse(); + } + + @Test + void testMatches_WithEnumerablePropertySource_ShouldReturnTrue() { + + @SuppressWarnings("unchecked") + EnumerablePropertySource> propertySource = + mock(EnumerablePropertySource.class); + when(propertySource.getPropertyNames()) + .thenReturn( + new String[] {"spring.security.oauth2.client.registration.test-client.client-id"}); + + when(environment.getPropertySources()).thenReturn(new MockPropertySources(propertySource)); + + boolean result = condition.matches(context, metadata); + assertThat(result).isTrue(); + } + + @Test + void testMatches_WithEnumerablePropertySourceButNoMatchingProperties_ShouldReturnFalse() { + + @SuppressWarnings("unchecked") + EnumerablePropertySource> propertySource = + mock(EnumerablePropertySource.class); + when(propertySource.getPropertyNames()).thenReturn(new String[] {"some.unrelated.property"}); + + when(environment.getPropertySources()).thenReturn(new MockPropertySources(propertySource)); + + boolean result = condition.matches(context, metadata); + assertThat(result).isFalse(); + } + + // Helper class to mock property sources + private static class MockPropertySources + extends org.springframework.core.env.MutablePropertySources { + MockPropertySources(PropertySource... propertySources) { + for (PropertySource ps : propertySources) { + addLast(ps); + } + } + } +} diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java new file mode 100644 index 0000000000..887f324833 --- /dev/null +++ b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2025 OpsMx, 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.oauth2; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.netflix.spinnaker.fiat.model.resources.ServiceAccount; +import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.gate.services.internal.Front50Service; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import retrofit2.mock.Calls; + +public class OAuthUserInfoServiceHelperTest { + + @InjectMocks private OAuthUserInfoServiceHelper userInfoService; + + @Mock private OAuth2SsoConfig.UserInfoMapping userInfoMapping; + + @Mock private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; + + @Mock private PermissionService permissionService; + + @Mock private Front50Service front50Service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void shouldEvaluateUserInfoRequirementsAgainstAuthenticationDetails() { + + // No domain restriction, everything should match + Map requirements = new HashMap<>(); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of())).isTrue(); + requirements = Map.of("hd", "foo.com"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); + requirements = Map.of("bar", "foo.com"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isTrue(); + requirements = Map.of("bar", "bar.com"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isTrue(); + + // Domain restricted but not found + requirements = Map.of("hd", "foo.com"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of())).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isFalse(); + + // Domain restricted by regex + requirements = Map.of("hd", "/foo\\.com|bar\\.com/"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of())).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "bar.com"))).isTrue(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "baz.com"))).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isFalse(); + + // Multiple restriction values + requirements = Map.of("bar", "bar.com"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isTrue(); + assertThat( + userInfoService.hasAllUserInfoRequirements(Map.of("hd", "foo.com", "bar", "bar.com"))) + .isTrue(); + + // Evaluating a list + requirements = Map.of("roles", "expected-role"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("roles", "expected-role"))) + .isTrue(); + assertThat( + userInfoService.hasAllUserInfoRequirements( + Map.of("roles", List.of("expected-role", "unexpected-role")))) + .isTrue(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of())).isFalse(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("roles", "unexpected-role"))) + .isFalse(); + assertThat( + userInfoService.hasAllUserInfoRequirements(Map.of("roles", List.of("unexpected-role")))) + .isFalse(); + + // Evaluating a regex in a list + requirements = Map.of("roles", "/^.+_ADMIN$/"); + when(userInfoRequirements.entrySet()).thenReturn(requirements.entrySet()); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("roles", "foo_ADMIN"))).isTrue(); + assertThat(userInfoService.hasAllUserInfoRequirements(Map.of("roles", List.of("foo_ADMIN")))) + .isTrue(); + assertThat( + userInfoService.hasAllUserInfoRequirements( + Map.of("roles", List.of("_ADMIN", "foo_USER")))) + .isFalse(); + assertThat( + userInfoService.hasAllUserInfoRequirements( + Map.of("roles", List.of("foo_ADMINISTRATOR", "bar_USER")))) + .isFalse(); + } + + @Test + void testIsServiceAccountValidServiceAccount() { + Map details = new HashMap<>(); + details.put("email", "service@example.com"); + + when(userInfoMapping.getServiceAccountEmail()).thenReturn("email"); + when(permissionService.isEnabled()).thenReturn(true); + ServiceAccount serviceAccount = new ServiceAccount(); + serviceAccount = serviceAccount.setName("service@example.com"); + when(front50Service.getServiceAccounts()).thenReturn(Calls.response(List.of(serviceAccount))); + + assertThat(userInfoService.isServiceAccount(details)).isTrue(); + } + + @Test + void testIsServiceAccountNotAServiceAccount() { + Map details = new HashMap<>(); + details.put("email", "user@example.com"); + + when(userInfoMapping.getServiceAccountEmail()).thenReturn("email"); + when(permissionService.isEnabled()).thenReturn(true); + ServiceAccount serviceAccount = new ServiceAccount(); + serviceAccount = serviceAccount.setName("service@example.com"); + when(front50Service.getServiceAccounts()).thenReturn(Calls.response(List.of(serviceAccount))); + + assertThat(userInfoService.isServiceAccount(details)).isFalse(); + } + + @ParameterizedTest + @MethodSource("provideRoleData") + public void shouldExtractRolesFromDetails(Object rolesValue, List expectedRoles) { + Map details = new HashMap<>(); + details.put("roles", rolesValue); + when(userInfoMapping.getRoles()).thenReturn("roles"); + assertThat(userInfoService.getRoles(details)).isEqualTo(expectedRoles); + } + + private static Stream provideRoleData() { + return Stream.of( + Arguments.of(null, List.of()), + Arguments.of("", List.of()), + Arguments.of(List.of("foo", "bar"), List.of("foo", "bar")), + Arguments.of("foo,bar", List.of("foo", "bar")), + Arguments.of("foo bar", List.of("foo", "bar")), + Arguments.of("foo", List.of("foo")), + Arguments.of("foo bar", List.of("foo", "bar")), + Arguments.of("foo,,,bar", List.of("foo", "bar")), + Arguments.of("foo, bar", List.of("foo", "bar")), + Arguments.of(List.of("[]"), List.of()), + Arguments.of(List.of("[\"foo\"]"), List.of("foo")), + Arguments.of(List.of("[\"foo\", \"bar\"]"), List.of("foo", "bar")), + Arguments.of(1, List.of()), + Arguments.of(Map.of("blergh", "blarg"), List.of())); + } +} diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServicesTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServicesTest.java deleted file mode 100644 index 51b48f8872..0000000000 --- a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerUserInfoTokenServicesTest.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2025 OpsMx, 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.oauth2; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.netflix.spinnaker.fiat.model.resources.ServiceAccount; -import com.netflix.spinnaker.gate.services.PermissionService; -import com.netflix.spinnaker.gate.services.internal.Front50Service; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import retrofit2.mock.Calls; - -public class SpinnakerUserInfoTokenServicesTest { - - private SpinnakerUserInfoTokenServices tokenServices; - private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; - - @BeforeEach - public void setUp() { - userInfoRequirements = new OAuth2SsoConfig.UserInfoRequirements(); - tokenServices = - new SpinnakerUserInfoTokenServices( - null, - null, - null, - null, - userInfoRequirements, - null, - null, - Optional.empty(), - null, - null, - null); - } - - @Test - public void shouldEvaluateUserInfoRequirementsAgainstAuthenticationDetails() { - // No domain restriction, everything should match - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of())).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isTrue(); - - // Domain restricted but not found - userInfoRequirements.put("hd", "foo.com"); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of())).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isFalse(); - - // Domain restricted by regex - userInfoRequirements.put("hd", "/foo\\.com|bar\\.com/"); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of())).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "bar.com"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "baz.com"))).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "foo.com"))).isFalse(); - - // Multiple restriction values - userInfoRequirements.put("bar", "bar.com"); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "foo.com"))).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("bar", "bar.com"))).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("hd", "foo.com", "bar", "bar.com"))) - .isTrue(); - - // Evaluating a list - userInfoRequirements.clear(); - userInfoRequirements.put("roles", "expected-role"); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("roles", "expected-role"))).isTrue(); - assertThat( - tokenServices.hasAllUserInfoRequirements( - Map.of("roles", List.of("expected-role", "unexpected-role")))) - .isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of())).isFalse(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("roles", "unexpected-role"))) - .isFalse(); - assertThat( - tokenServices.hasAllUserInfoRequirements(Map.of("roles", List.of("unexpected-role")))) - .isFalse(); - - // Evaluating a regex in a list - userInfoRequirements.put("roles", "/^.+_ADMIN$/"); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("roles", "foo_ADMIN"))).isTrue(); - assertThat(tokenServices.hasAllUserInfoRequirements(Map.of("roles", List.of("foo_ADMIN")))) - .isTrue(); - assertThat( - tokenServices.hasAllUserInfoRequirements( - Map.of("roles", List.of("_ADMIN", "foo_USER")))) - .isFalse(); - assertThat( - tokenServices.hasAllUserInfoRequirements( - Map.of("roles", List.of("foo_ADMINISTRATOR", "bar_USER")))) - .isFalse(); - } - - @Test - public void verifyIsServiceAccount() { - PermissionService permissionService = mock(PermissionService.class); - when(permissionService.isEnabled()).thenReturn(true); - - Front50Service front50Service = mock(Front50Service.class); - ServiceAccount serviceAccount = new ServiceAccount(); - serviceAccount = serviceAccount.setName("ex@foo.com"); - when(front50Service.getServiceAccounts()).thenReturn(Calls.response(List.of(serviceAccount))); - - SpinnakerUserInfoTokenServices tokenServices = - new SpinnakerUserInfoTokenServices( - null, - null, - null, - new OAuth2SsoConfig.UserInfoMapping(), - null, - permissionService, - front50Service, - Optional.empty(), - null, - null, - null); - - Map details = Map.of("client_email", "ex@foo.com"); - boolean isServiceAccount = tokenServices.isServiceAccount(details); - - assertThat(isServiceAccount).isTrue(); - verify(permissionService).isEnabled(); - verify(front50Service).getServiceAccounts(); - } - - @ParameterizedTest - @MethodSource("provideRoleData") - public void shouldExtractRolesFromDetails(Object rolesValue, List expectedRoles) { - OAuth2SsoConfig.UserInfoMapping userInfoMapping = new OAuth2SsoConfig.UserInfoMapping(); - userInfoMapping.setRoles("roles"); - SpinnakerUserInfoTokenServices tokenServices = - new SpinnakerUserInfoTokenServices( - null, - null, - null, - userInfoMapping, - null, - null, - null, - Optional.empty(), - null, - null, - null); - - Map details = new HashMap<>(); - details.put("roles", rolesValue); - assertThat(tokenServices.getRoles(details)).isEqualTo(expectedRoles); - } - - private static Stream provideRoleData() { - return Stream.of( - Arguments.of(null, List.of()), - Arguments.of("", List.of()), - Arguments.of(List.of("foo", "bar"), List.of("foo", "bar")), - Arguments.of("foo,bar", List.of("foo", "bar")), - Arguments.of("foo bar", List.of("foo", "bar")), - Arguments.of("foo", List.of("foo")), - Arguments.of("foo bar", List.of("foo", "bar")), - Arguments.of("foo,,,bar", List.of("foo", "bar")), - Arguments.of("foo, bar", List.of("foo", "bar")), - Arguments.of(List.of("[]"), List.of()), - Arguments.of(List.of("[\"foo\"]"), List.of("foo")), - Arguments.of(List.of("[\"foo\", \"bar\"]"), List.of("foo", "bar")), - Arguments.of(1, List.of()), - Arguments.of(Map.of("blergh", "blarg"), List.of())); - } -} diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java index 4144653576..e19c9fc886 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java @@ -34,8 +34,8 @@ @AutoConfigureMockMvc @SpringBootTest( properties = { - "security.oauth2.client.clientId=Spinnaker-Client", - "security.oauth2.resource.userInfoUri=http://localhost/userinfo" + "spring.security.oauth2.client.registration.github.client-id=ec415f229e8f06f6ddb", + "spring.security.oauth2.client.registration.github.client-secret=53dc2b2125d356c652dfb83fbc0d209de4a9f60" }) @TestPropertySource(properties = {"spring.config.location=classpath:gate-test.yml"}) public class OAuth2Test { From 1092acc581044b0f2a0f23e70581506801d368be Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Thu, 13 Mar 2025 22:18:52 +0530 Subject: [PATCH 02/16] Add javadoc --- .../security/oauth2/OAuthConfigEnabled.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java index 77a073e713..bb58ae74fb 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthConfigEnabled.java @@ -25,6 +25,35 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.type.AnnotatedTypeMetadata; +/** + * This class implements the {@link Condition} interface to check if OAuth2 configuration is enabled + * in the Spring environment based on the presence of a client ID property. It inspects the + * environment's property sources for any property matching the regular expression for OAuth2 client + * registration. + * + *

The condition matches if there is any property key that matches the pattern: + * "spring.security.oauth2.client.registration..client-id". If such a property exists, + * the condition returns {@code true}, indicating that OAuth2 configuration is enabled. Otherwise, + * it returns {@code false}. + * + *

This condition can be used in Spring's {@link + * org.springframework.context.annotation.Conditional} annotations to conditionally enable or + * disable beans based on the presence of OAuth2 client properties in the application's + * configuration. + * + *

Example: + * + *

+ * @Configuration
+ * @Conditional(OAuthConfigEnabled.class)
+ * public class OAuth2SsoConfig {
+ *     // Bean definitions for OAuth2 configuration
+ * }
+ * 
+ * + *

Note: The condition looks for the client ID property in the environment, which is a standard + * property for configuring OAuth2 client registration in Spring Security. + */ @Slf4j public class OAuthConfigEnabled implements Condition { private static final String SPRING_SECURITY_OAUTH2_REGEX = @@ -32,6 +61,23 @@ public class OAuthConfigEnabled implements Condition { private static final Pattern SPRING_SECURITY_OAUTH2_PATTERN = Pattern.compile(SPRING_SECURITY_OAUTH2_REGEX); + /** + * Evaluates whether the condition matches based on the presence of OAuth2 client registration + * properties in the Spring environment. + * + *

This method checks if the application's {@link ConfigurableEnvironment} contains any + * property names that match the pattern + * spring.security.oauth2.client.registration.<client-name>.client-id. If at least + * one such property exists, the method returns {@code true}, indicating that OAuth2 configuration + * is enabled. Otherwise, it returns {@code false}. + * + * @param context The {@link ConditionContext}, which provides access to the Spring environment + * and application context. + * @param metadata The {@link AnnotatedTypeMetadata} of the annotated component. (Not used in this + * implementation.) + * @return {@code true} if at least one OAuth2 client registration property is found, {@code + * false} otherwise. + */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (!(context.getEnvironment() instanceof ConfigurableEnvironment)) { From 1f9f38401dc1aec122b5b831257d6bf84b9307ce Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Thu, 13 Mar 2025 22:20:19 +0530 Subject: [PATCH 03/16] Use Objects.toString instead of custom toStringOrNull method --- .../oauth2/OAuthUserInfoServiceHelper.java | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index c9f642acee..a645cc3b4a 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; import java.util.regex.Pattern; @@ -102,7 +103,7 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u } } - final String username = toStringOrNull(details.get(userInfoMapping.getUsername())); + final String username = Objects.toString(details.get(userInfoMapping.getUsername()), null); List roles = Optional.ofNullable(groupExtractor) .map(extractor -> extractor.apply(accessToken, details)) @@ -154,9 +155,9 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u if (oAuth2User instanceof OidcUser oidcUser) { SpinnakerOIDCUser spinnakerUser = new SpinnakerOIDCUser( - toStringOrNull(details.get(userInfoMapping.getEmail())), - toStringOrNull(details.get(userInfoMapping.getFirstName())), - toStringOrNull(details.get(userInfoMapping.getLastName())), + Objects.toString(details.get(userInfoMapping.getEmail()), null), + Objects.toString(details.get(userInfoMapping.getFirstName()), null), + Objects.toString(details.get(userInfoMapping.getLastName()), null), allowedAccountsSupport.filterAllowedAccounts(username, roles), roles, username, @@ -169,9 +170,9 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u } else { SpinnakerOAuth2User spinnakerUser = new SpinnakerOAuth2User( - toStringOrNull(details.get(userInfoMapping.getEmail())), - toStringOrNull(details.get(userInfoMapping.getFirstName())), - toStringOrNull(details.get(userInfoMapping.getLastName())), + Objects.toString(details.get(userInfoMapping.getEmail()), null), + Objects.toString(details.get(userInfoMapping.getFirstName()), null), + Objects.toString(details.get(userInfoMapping.getLastName()), null), allowedAccountsSupport.filterAllowedAccounts(username, roles), roles, username); @@ -303,17 +304,4 @@ private List parseJsonRoles(String jsonString) { return List.of(); } } - /** - * Safely converts an object to a string representation. - * - *

This method checks if the provided object is non-null before calling {@code toString()}. If - * the object is {@code null}, it returns {@code null} instead of throwing a {@code - * NullPointerException}. - * - * @param o the object to convert to a string, may be {@code null} - * @return the string representation of the object, or {@code null} if the object is {@code null} - */ - static String toStringOrNull(Object o) { - return o != null ? o.toString() : null; - } } From 3e64fb64c00b104aeaab1c6c050a273a6da25e1b Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Fri, 14 Mar 2025 06:16:16 +0530 Subject: [PATCH 04/16] Remove unused class member --- .../netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java index b7bbbf49d5..a08abaee6a 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuth2SsoConfig.java @@ -26,7 +26,6 @@ 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.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.stereotype.Component; @@ -40,7 +39,6 @@ public class OAuth2SsoConfig extends WebSecurityConfigurerAdapter { @Autowired private SpinnakerOAuth2UserInfoService customOAuth2UserService; @Autowired private SpinnakerOIDCUserInfoService oidcUserInfoService; @Autowired private DefaultCookieSerializer defaultCookieSerializer; - @Autowired private ClientRegistrationRepository clientRegistrationRepository; @Override public void configure(HttpSecurity httpSecurity) throws Exception { From 0207ced4159a6239f11fc72d502f6991f8edfdf4 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Fri, 14 Mar 2025 06:17:39 +0530 Subject: [PATCH 05/16] Add construtor injection --- .../oauth2/OAuthUserInfoServiceHelper.java | 36 ++++++++++++++----- .../SpinnakerOAuth2UserInfoService.java | 7 +++- .../oauth2/SpinnakerOIDCUserInfoService.java | 7 +++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index a645cc3b4a..33adaaa09c 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -59,22 +59,22 @@ @Slf4j public class OAuthUserInfoServiceHelper { - @Autowired private OAuth2SsoConfig.UserInfoMapping userInfoMapping; + private OAuth2SsoConfig.UserInfoMapping userInfoMapping; - @Autowired private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; + private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; - @Autowired private PermissionService permissionService; + private PermissionService permissionService; - @Autowired private Front50Service front50Service; + private Front50Service front50Service; - @Autowired(required = false) - private SpinnakerProviderTokenServices providerTokenServices; + private AllowedAccountsSupport allowedAccountsSupport; - @Autowired private AllowedAccountsSupport allowedAccountsSupport; + private FiatClientConfigurationProperties fiatClientConfigurationProperties; - @Autowired private FiatClientConfigurationProperties fiatClientConfigurationProperties; + private Registry registry; - @Autowired private Registry registry; + @Autowired(required = false) + private SpinnakerProviderTokenServices providerTokenServices; @Autowired(required = false) @Qualifier("spinnaker-oauth2-group-extractor") @@ -82,6 +82,24 @@ public class OAuthUserInfoServiceHelper { private final RetrySupport retrySupport = new RetrySupport(); + @Autowired + public OAuthUserInfoServiceHelper( + OAuth2SsoConfig.UserInfoMapping userInfoMapping, + OAuth2SsoConfig.UserInfoRequirements userInfoRequirements, + PermissionService permissionService, + Front50Service front50Service, + AllowedAccountsSupport allowedAccountsSupport, + FiatClientConfigurationProperties fiatClientConfigurationProperties, + Registry registry) { + this.userInfoMapping = userInfoMapping; + this.userInfoRequirements = userInfoRequirements; + this.permissionService = permissionService; + this.front50Service = front50Service; + this.allowedAccountsSupport = allowedAccountsSupport; + this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; + this.registry = registry; + } + T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest userRequest) { Map details = oAuth2User.getAttributes(); diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java index a182416334..4d0b813591 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java @@ -26,7 +26,12 @@ @Service @Slf4j public class SpinnakerOAuth2UserInfoService extends DefaultOAuth2UserService { - @Autowired private OAuthUserInfoServiceHelper userInfoService; + private OAuthUserInfoServiceHelper userInfoService; + + @Autowired + public SpinnakerOAuth2UserInfoService(OAuthUserInfoServiceHelper userInfoService) { + this.userInfoService = userInfoService; + } @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) { diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java index 2442c9a4c0..6c4a32371b 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java @@ -26,7 +26,12 @@ @Service @Slf4j public class SpinnakerOIDCUserInfoService extends OidcUserService { - @Autowired private OAuthUserInfoServiceHelper userInfoService; + private OAuthUserInfoServiceHelper userInfoService; + + @Autowired + public SpinnakerOIDCUserInfoService(OAuthUserInfoServiceHelper userInfoService) { + this.userInfoService = userInfoService; + } @Override public OidcUser loadUser(OidcUserRequest userRequest) { From 49dcc5a124115ba83c1c5db1b05a40c1643f2dd3 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Fri, 14 Mar 2025 06:18:20 +0530 Subject: [PATCH 06/16] Call get property than the direct access --- .../spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java | 2 +- .../spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java index 0ddaefecbd..8fcf245efa 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java @@ -56,6 +56,6 @@ public List getAuthorities() { @Override public String getName() { - return super.username; + return super.getUsername(); } } diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java index ca7e6f96ec..15b52e4e81 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java @@ -64,7 +64,7 @@ public List getAuthorities() { @Override public String getName() { - return super.username; + return super.getUsername(); } @Override From af1c8db074110983aa80a588155e4b81c68de872 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Tue, 18 Mar 2025 16:30:35 +0530 Subject: [PATCH 07/16] Add javadoc, made class level properties final, Add GithubProviderTokenServices, Made authorities and attributes immutable --- .../oauth2/OAuthUserInfoServiceHelper.java | 29 +++--- .../security/oauth2/SpinnakerOAuth2User.java | 38 ++++++- .../SpinnakerOAuth2UserInfoService.java | 14 +++ .../security/oauth2/SpinnakerOIDCUser.java | 42 +++++++- .../oauth2/SpinnakerOIDCUserInfoService.java | 11 +++ .../provider/GithubProviderTokenServices.java | 99 +++++++++++++++++++ 6 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index 33adaaa09c..973632fb05 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -59,19 +59,19 @@ @Slf4j public class OAuthUserInfoServiceHelper { - private OAuth2SsoConfig.UserInfoMapping userInfoMapping; + private final OAuth2SsoConfig.UserInfoMapping userInfoMapping; - private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; + private final OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; - private PermissionService permissionService; + private final PermissionService permissionService; - private Front50Service front50Service; + private final Front50Service front50Service; - private AllowedAccountsSupport allowedAccountsSupport; + private final AllowedAccountsSupport allowedAccountsSupport; - private FiatClientConfigurationProperties fiatClientConfigurationProperties; + private final FiatClientConfigurationProperties fiatClientConfigurationProperties; - private Registry registry; + private final Registry registry; @Autowired(required = false) private SpinnakerProviderTokenServices providerTokenServices; @@ -180,9 +180,10 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u roles, username, oidcUser.getIdToken(), - oidcUser.getUserInfo()); - spinnakerUser.getAttributes().putAll(details); - spinnakerUser.getAuthorities().addAll(oAuth2User.getAuthorities()); + oidcUser.getUserInfo(), + details, + Optional.ofNullable(oidcUser.getAuthorities()).orElse(new ArrayList<>()).stream() + .collect(Collectors.toList())); return (T) spinnakerUser; } else { @@ -193,10 +194,10 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u Objects.toString(details.get(userInfoMapping.getLastName()), null), allowedAccountsSupport.filterAllowedAccounts(username, roles), roles, - username); - spinnakerUser.getAttributes().putAll(details); - spinnakerUser.getAuthorities().addAll(oAuth2User.getAuthorities()); - + username, + details, + Optional.ofNullable(oAuth2User.getAuthorities()).orElse(new ArrayList<>()).stream() + .collect(Collectors.toList())); return (T) spinnakerUser; } } diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java index 8fcf245efa..a8edea7962 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java @@ -19,15 +19,35 @@ import com.netflix.spinnaker.security.User; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; +/** + * Custom implementation of {@link OAuth2User} that integrates with Spinnaker's {@link User} model. + * This class holds OAuth2-related user details and provides attributes such as roles and allowed + * accounts. + * + *

It extends {@link User} from Netflix Spinnaker to include Spinnaker-specific fields. + * + *

Usage: This class is used in OAuth2 authentication flows where user details are retrieved from + * an OAuth2 provider. + * + * @author rahul-chekuri + * @see User + */ public class SpinnakerOAuth2User extends User implements OAuth2User { - private Map attribute = new HashMap<>(); - private List authorities = new ArrayList<>(); + /** + * Attributes containing user details, retrieved from the OIDC provider. These attributes + * typically include user profile information such as name, email, and roles. + */ + private final Map attributes; + + /** Authorities assigned to the user, used for authorization in Spring Security. */ + private final List authorities; public SpinnakerOAuth2User( String email, @@ -35,18 +55,28 @@ public SpinnakerOAuth2User( String lastName, Collection allowedAccounts, List roles, - String username) { + String username, + Map attributes, + List authorities) { this.email = email; this.firstName = firstName; this.lastName = lastName; this.allowedAccounts = allowedAccounts; this.roles = roles; this.username = username; + this.attributes = + attributes != null + ? Collections.unmodifiableMap(new HashMap<>(attributes)) + : Collections.emptyMap(); + this.authorities = + authorities != null + ? Collections.unmodifiableList(new ArrayList<>(authorities)) + : Collections.emptyList(); } @Override public Map getAttributes() { - return attribute; + return attributes; } @Override diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java index 4d0b813591..0d58364206 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2UserInfoService.java @@ -23,6 +23,20 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +/** + * Custom OAuth2 user service that extends {@link DefaultOAuth2UserService} to load and process + * OAuth2 user details. This service integrates with {@link OAuthUserInfoServiceHelper} to transform + * and return the Spinnaker-specific OAuth2 user object. + * + *

Overrides the {@link #loadUser(OAuth2UserRequest)} method to modify user details after + * retrieving them from the OAuth2 provider. + * + *

Usage: This service is automatically registered as a Spring Bean and is used during OAuth2 + * authentication. + * + * @author rahul-chekuri + * @see OAuthUserInfoServiceHelper + */ @Service @Slf4j public class SpinnakerOAuth2UserInfoService extends DefaultOAuth2UserService { diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java index 15b52e4e81..b10b479c04 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java @@ -19,6 +19,7 @@ import com.netflix.spinnaker.security.User; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,9 +28,30 @@ import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +/** + * Custom implementation of {@link OidcUser} that integrates with Spinnaker's {@link User} model. + * This class holds OIDC-related user details such as ID token, user info, and additional + * attributes. + * + *

It extends {@link User} from kork to include Spinnaker-specific fields like allowed accounts + * and roles. + * + *

Usage: This class is used in OIDC authentication flows where user details are retrieved from + * an OIDC provider. + * + * @author rahul-chekuri + * @see User + */ public class SpinnakerOIDCUser extends User implements OidcUser { - private Map attribute = new HashMap<>(); - private List authorities = new ArrayList<>(); + /** + * Attributes containing user details, retrieved from the OIDC provider. These attributes + * typically include user profile information such as name, email, and roles. + */ + private final Map attributes; + + /** Authorities assigned to the user, used for authorization in Spring Security. */ + private final List authorities; + private final OidcIdToken idToken; private final OidcUserInfo userInfo; @@ -41,7 +63,9 @@ public SpinnakerOIDCUser( List roles, String username, OidcIdToken idToken, - OidcUserInfo userInfo) { + OidcUserInfo userInfo, + Map attributes, + List authorities) { this.idToken = idToken; this.userInfo = userInfo; this.email = email; @@ -50,11 +74,19 @@ public SpinnakerOIDCUser( this.allowedAccounts = allowedAccounts; this.roles = roles; this.username = username; + this.attributes = + attributes != null + ? Collections.unmodifiableMap(new HashMap<>(attributes)) + : Collections.emptyMap(); + this.authorities = + authorities != null + ? Collections.unmodifiableList(new ArrayList<>(authorities)) + : Collections.emptyList(); } @Override public Map getAttributes() { - return attribute; + return attributes; } @Override @@ -69,7 +101,7 @@ public String getName() { @Override public Map getClaims() { - return this.attribute; + return this.attributes; } @Override diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java index 6c4a32371b..d04b8a2248 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUserInfoService.java @@ -23,6 +23,17 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; +/** + * Custom OIDC user service that extends {@link OidcUserService} to load and process OIDC user + * details. This service integrates with {@link OAuthUserInfoServiceHelper} to transform and return + * the Spinnaker-specific OIDC user object. + * + *

Overrides the {@link #loadUser(OidcUserRequest)} method to modify user details after + * retrieving them from the OpenID Connect (OIDC) provider. + * + * @author rahul-chekuri + * @see OAuthUserInfoServiceHelper + */ @Service @Slf4j public class SpinnakerOIDCUserInfoService extends OidcUserService { diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java new file mode 100644 index 0000000000..e61f13e538 --- /dev/null +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025 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.oauth2.provider; + +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * This class implements {@link SpinnakerProviderTokenServices} to verify if a user meets the + * provider-specific authentication requirements for GitHub. + * + *

It checks if a user is a member of a required GitHub organization before granting access. + */ +@Slf4j +@Component +@ConditionalOnProperty(name = "spring.security.oauth2.client.registration.github.client-id") +public class GithubProviderTokenServices implements SpinnakerProviderTokenServices { + + @Autowired private GithubRequirements requirements; + + private boolean githubOrganizationMember( + String organization, List> organizations) { + for (Map org : organizations) { + if (organization.equals(org.get("login"))) { + return true; + } + } + return false; + } + + private boolean checkOrganization( + String accessToken, String organizationsUrl, String organization) { + try { + log.debug("Getting user organizations from URL {}", organizationsUrl); + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + headers.set("Accept", MediaType.APPLICATION_JSON_VALUE); + + ResponseEntity response = + restTemplate.exchange( + organizationsUrl, HttpMethod.GET, new HttpEntity<>(headers), List.class); + + List> organizations = response.getBody(); + return githubOrganizationMember(organization, organizations); + } catch (Exception e) { + log.warn("Could not fetch user organizations", e); + return false; + } + } + + public boolean hasAllProviderRequirements(String token, Map details) { + boolean hasRequirements = true; + if (requirements.getOrganization() != null && details.containsKey("organizations_url")) { + boolean orgMatch = + checkOrganization( + token, (String) details.get("organizations_url"), requirements.getOrganization()); + if (!orgMatch) { + log.debug("User does not include required organization {}", requirements.getOrganization()); + hasRequirements = false; + } + } + return hasRequirements; + } + + @Component + @ConfigurationProperties( + "spring.security.oauth2.client.registration.github.provider-requirements") + @Data + public static class GithubRequirements { + private String organization; + } +} From 2b67a513a18c7121e81510a3ed10573acb5603db Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Thu, 20 Mar 2025 15:04:40 +0530 Subject: [PATCH 08/16] Enhance null attributes and authorities logic. Correct Copyright. Add javadoc to getClaims method. Add test coverage to GithubProviderTokenServices --- .../oauth2/OAuthUserInfoServiceHelper.java | 6 ++---- .../security/oauth2/SpinnakerOAuth2User.java | 2 +- .../gate/security/oauth2/SpinnakerOIDCUser.java | 17 ++++++++++++++++- .../provider/GithubProviderTokenServices.java | 2 +- .../gate/security/oauth/OAuth2Test.java | 14 ++++++++++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index 973632fb05..630c62c41c 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -182,8 +182,7 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u oidcUser.getIdToken(), oidcUser.getUserInfo(), details, - Optional.ofNullable(oidcUser.getAuthorities()).orElse(new ArrayList<>()).stream() - .collect(Collectors.toList())); + oidcUser.getAuthorities()); return (T) spinnakerUser; } else { @@ -196,8 +195,7 @@ T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest u roles, username, details, - Optional.ofNullable(oAuth2User.getAuthorities()).orElse(new ArrayList<>()).stream() - .collect(Collectors.toList())); + oAuth2User.getAuthorities()); return (T) spinnakerUser; } } diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java index a8edea7962..d46bd24051 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOAuth2User.java @@ -57,7 +57,7 @@ public SpinnakerOAuth2User( List roles, String username, Map attributes, - List authorities) { + Collection authorities) { this.email = email; this.firstName = firstName; this.lastName = lastName; diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java index b10b479c04..2c6e72c091 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/SpinnakerOIDCUser.java @@ -65,7 +65,7 @@ public SpinnakerOIDCUser( OidcIdToken idToken, OidcUserInfo userInfo, Map attributes, - List authorities) { + Collection authorities) { this.idToken = idToken; this.userInfo = userInfo; this.email = email; @@ -99,6 +99,21 @@ public String getName() { return super.getUsername(); } + /** + * Returns the claims associated with the authenticated user. + * + *

This method returns the same attributes map as {@link #getAttributes()} because, in OIDC, + * claims typically refer to user attributes retrieved from the identity provider. This approach + * aligns with the implementation in {@link + * org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser}, which also returns the + * attributes map for claims. + * + *

By returning attributes as claims, we ensure consistency with how Spring Security processes + * OIDC user details and maintains compatibility with components expecting claims to be retrieved + * from this method. + * + * @return a map containing the user's claims + */ @Override public Map getClaims() { return this.attributes; diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java index e61f13e538..be33c960d7 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/provider/GithubProviderTokenServices.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 Netflix, Inc. + * Copyright 2025 OpsMx, Inc. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. diff --git a/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java b/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java index e19c9fc886..0f7be90cbc 100644 --- a/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java +++ b/gate-web/src/test/java/com/netflix/spinnaker/gate/security/oauth/OAuth2Test.java @@ -22,6 +22,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.netflix.spinnaker.gate.security.oauth2.provider.SpinnakerProviderTokenServices; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -34,14 +36,21 @@ @AutoConfigureMockMvc @SpringBootTest( properties = { - "spring.security.oauth2.client.registration.github.client-id=ec415f229e8f06f6ddb", - "spring.security.oauth2.client.registration.github.client-secret=53dc2b2125d356c652dfb83fbc0d209de4a9f60" + "spring.security.oauth2.client.registration.github.client-id=client-id", + "spring.security.oauth2.client.registration.github.client-secret=client-secret" }) @TestPropertySource(properties = {"spring.config.location=classpath:gate-test.yml"}) public class OAuth2Test { @Autowired private MockMvc mockMvc; + /** + * This property is used to test the creation of the `GithubProviderTokenServices` bean when the + * `spring.security.oauth2.client.registration.github.client-id` property is present in the + * configuration. fails if GithubProviderTokenServices bean is unavailable in the context + */ + @Autowired private SpinnakerProviderTokenServices providerTokenServices; + @Test void shouldRedirectOnOauth2Authentication() throws Exception { MvcResult result = @@ -51,6 +60,7 @@ void shouldRedirectOnOauth2Authentication() throws Exception { .andReturn(); assertEquals(302, result.getResponse().getStatus()); + Assertions.assertThat(providerTokenServices).isNotNull(); } /** Test: Public endpoint should be accessible without authentication */ From 3efc6add15f73b5ac57b45aae93bcb4fc749d6f9 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Tue, 1 Apr 2025 14:04:02 +0530 Subject: [PATCH 09/16] Use constructor injection for SpinnakerProviderTokenServices --- .../oauth2/OAuthUserInfoServiceHelper.java | 8 ++++++-- .../oauth2/OAuthUserInfoServiceHelperTest.java | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java index 630c62c41c..a5c2da238d 100644 --- a/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java +++ b/gate-oauth2/src/main/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelper.java @@ -73,7 +73,6 @@ public class OAuthUserInfoServiceHelper { private final Registry registry; - @Autowired(required = false) private SpinnakerProviderTokenServices providerTokenServices; @Autowired(required = false) @@ -90,7 +89,8 @@ public OAuthUserInfoServiceHelper( Front50Service front50Service, AllowedAccountsSupport allowedAccountsSupport, FiatClientConfigurationProperties fiatClientConfigurationProperties, - Registry registry) { + Registry registry, + Optional providerTokenServices) { this.userInfoMapping = userInfoMapping; this.userInfoRequirements = userInfoRequirements; this.permissionService = permissionService; @@ -98,6 +98,10 @@ public OAuthUserInfoServiceHelper( this.allowedAccountsSupport = allowedAccountsSupport; this.fiatClientConfigurationProperties = fiatClientConfigurationProperties; this.registry = registry; + + if (providerTokenServices.isPresent()) { + this.providerTokenServices = providerTokenServices.get(); + } } T getOAuthSpinnakerUser(T oAuth2User, OAuth2UserRequest userRequest) { diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java index 887f324833..05ba0d2368 100644 --- a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java +++ b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java @@ -25,21 +25,19 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import retrofit2.mock.Calls; public class OAuthUserInfoServiceHelperTest { - @InjectMocks private OAuthUserInfoServiceHelper userInfoService; - @Mock private OAuth2SsoConfig.UserInfoMapping userInfoMapping; @Mock private OAuth2SsoConfig.UserInfoRequirements userInfoRequirements; @@ -48,9 +46,22 @@ public class OAuthUserInfoServiceHelperTest { @Mock private Front50Service front50Service; + private OAuthUserInfoServiceHelper userInfoService; + @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); + // Manually instantiate the class to ensure correct injection + userInfoService = + new OAuthUserInfoServiceHelper( + userInfoMapping, + userInfoRequirements, + permissionService, + front50Service, + null, + null, + null, + Optional.empty()); } @Test From c08c328bb87568b60b0d2d3b3cd5878d56a1a6a2 Mon Sep 17 00:00:00 2001 From: rahul-chekuri Date: Wed, 2 Apr 2025 10:05:19 +0530 Subject: [PATCH 10/16] Tests: Refactor test initialization by replacing MockitoAnnotations.openMocks(this) with @ExtendWith(MockitoExtension.class) --- .../gate/security/oauth2/OAuthUserInfoServiceHelperTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java index 05ba0d2368..88d322cbc5 100644 --- a/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java +++ b/gate-oauth2/src/test/java/com/netflix/spinnaker/gate/security/oauth2/OAuthUserInfoServiceHelperTest.java @@ -29,13 +29,15 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import retrofit2.mock.Calls; +@ExtendWith(MockitoExtension.class) public class OAuthUserInfoServiceHelperTest { @Mock private OAuth2SsoConfig.UserInfoMapping userInfoMapping; @@ -50,7 +52,6 @@ public class OAuthUserInfoServiceHelperTest { @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); // Manually instantiate the class to ensure correct injection userInfoService = new OAuthUserInfoServiceHelper( From 7e9f3dadb2ac750c3b747e183fc8d1e61f3b664f Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 2 Apr 2025 22:36:34 -0400 Subject: [PATCH 11/16] chore(dependencies): Autobump korkVersion (#1889) * chore(dependencies): Autobump korkVersion * fix(retrofit2/test): deal with the introduction of Retrofit2EncodeCorrectionInterceptor adding beans as necessary. --------- Co-authored-by: root Co-authored-by: David Byron --- .../plugins/web/info/PluginInfoControllerSpec.groovy | 11 ++++++++++- .../com/netflix/spinnaker/gate/FunctionalSpec.groovy | 6 ++++++ gradle.properties | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/gate-plugins/src/test/groovy/com/netflix/spinnaker/gate/plugins/web/info/PluginInfoControllerSpec.groovy b/gate-plugins/src/test/groovy/com/netflix/spinnaker/gate/plugins/web/info/PluginInfoControllerSpec.groovy index 0df68685fe..d1f54afe47 100644 --- a/gate-plugins/src/test/groovy/com/netflix/spinnaker/gate/plugins/web/info/PluginInfoControllerSpec.groovy +++ b/gate-plugins/src/test/groovy/com/netflix/spinnaker/gate/plugins/web/info/PluginInfoControllerSpec.groovy @@ -36,10 +36,12 @@ import com.netflix.spinnaker.gate.services.internal.RoscoService import com.netflix.spinnaker.gate.services.internal.SwabbieService import com.netflix.spinnaker.kork.plugins.SpinnakerPluginManager import com.netflix.spinnaker.kork.web.exceptions.GenericExceptionHandlers +import com.netflix.spinnaker.okhttp.Retrofit2EncodeCorrectionInterceptor import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration @@ -56,11 +58,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Import(ErrorConfiguration) @WebMvcTest(controllers = [PluginInfoController]) @AutoConfigureMockMvc(addFilters = false) -@ContextConfiguration(classes = [PluginsAutoConfiguration.class,PluginInfoController, GenericExceptionHandlers, SpinnakerExtensionsConfigProperties, PluginWebConfiguration, ServiceConfiguration]) +@ContextConfiguration(classes = [PluginsAutoConfiguration, PluginInfoController, GenericExceptionHandlers, SpinnakerExtensionsConfigProperties, PluginWebConfiguration, ServiceConfiguration, PluginInfoControllerTestConfiguration]) @ActiveProfiles("test") @TestPropertySource(properties = ["spring.config.location=classpath:gate-test.yml"]) class PluginInfoControllerSpec extends Specification { + static class PluginInfoControllerTestConfiguration { + @Bean + Retrofit2EncodeCorrectionInterceptor retrofit2EncodeCorrectionInterceptor() { + return new Retrofit2EncodeCorrectionInterceptor(); + } + } + @Autowired private MockMvc mockMvc diff --git a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy index d132366ff7..15ff56ef07 100644 --- a/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy +++ b/gate-web/src/test/groovy/com/netflix/spinnaker/gate/FunctionalSpec.groovy @@ -33,6 +33,7 @@ import com.netflix.spinnaker.kork.dynamicconfig.SpringDynamicConfigService import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import com.netflix.spinnaker.okhttp.Retrofit2EncodeCorrectionInterceptor import okhttp3.OkHttpClient import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.EnableAutoConfiguration @@ -291,6 +292,11 @@ class FunctionalSpec extends Specification { new PipelineControllerConfigProperties(); } + @Bean + Retrofit2EncodeCorrectionInterceptor retrofit2EncodeCorrectionInterceptor() { + return new Retrofit2EncodeCorrectionInterceptor(); + } + @Override protected void configure(HttpSecurity http) throws Exception { http diff --git a/gradle.properties b/gradle.properties index cdbf7cf0b0..d96027330a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.54.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.251.0 +korkVersion=7.252.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 0d7820bf353a247dccae6e335c60e2fa3e79661a Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Wed, 2 Apr 2025 23:18:22 -0400 Subject: [PATCH 12/16] chore(dependencies): Autobump fiatVersion (#1890) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d96027330a..18616ee515 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.54.0 +fiatVersion=1.55.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.252.0 kotlinVersion=1.6.21 From fe8b522ce9ce728f195cd4565b82e60f3330eaa4 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 4 Apr 2025 19:40:23 -0400 Subject: [PATCH 13/16] chore(dependencies): Autobump korkVersion (#1891) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 18616ee515..884b9aac75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.55.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.252.0 +korkVersion=7.253.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From fb27b30774bf4abbfced386a03bab39e2ab01925 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Fri, 4 Apr 2025 20:50:16 -0400 Subject: [PATCH 14/16] chore(dependencies): Autobump fiatVersion (#1892) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 884b9aac75..12290c2a35 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.55.0 +fiatVersion=1.56.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.253.0 kotlinVersion=1.6.21 From 327c7ecd9d5faf417d4d7e4bf318690e2900605e Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 8 Apr 2025 14:21:15 -0400 Subject: [PATCH 15/16] chore(dependencies): Autobump korkVersion (#1894) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 12290c2a35..cd1251b7ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ enablePublishing=false fiatVersion=1.56.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 -korkVersion=7.253.0 +korkVersion=7.254.0 kotlinVersion=1.6.21 org.gradle.parallel=true spinnakerGradleVersion=8.32.1 From 4172fd0711f26727f2d2388b731a3bb697052ca3 Mon Sep 17 00:00:00 2001 From: spinnakerbot Date: Tue, 8 Apr 2025 16:10:59 -0400 Subject: [PATCH 16/16] chore(dependencies): Autobump fiatVersion (#1895) Co-authored-by: root --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cd1251b7ce..1b2b06081a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ enablePublishing=false -fiatVersion=1.56.0 +fiatVersion=1.57.0 includeProviders=basic,iap,ldap,oauth2,saml,x509 korkVersion=7.254.0 kotlinVersion=1.6.21