From 3913669b83f12f399d1bafb5ee72a8861de8da58 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Mon, 28 Oct 2024 16:48:36 -0400 Subject: [PATCH] Initial security migration Separate dataflow and skipper configs instead of trying to "reuse" common configs. Skipper config did extend classes from comman but then override everything. Remove unused CF configs related to tweaking things for "space developer" as this was never used. Essentially minimal changes to get new security configuration model working. There's still some deprecations left to handle but it's better to get better integration tests before those are handled. --- .../CommonSecurityAutoConfiguration.java | 4 +- .../security/OAuthClientConfiguration.java | 201 +++++++ .../security/OAuthSecurityConfiguration.java | 495 ------------------ .../security/support/SecurityConfigUtils.java | 17 +- ...loudFoundryOAuthSecurityConfiguration.java | 110 ---- .../security/support/AccessLevel.java | 72 --- .../CloudFoundryAuthorizationException.java | 91 ---- ...CloudFoundryDataflowAuthoritiesMapper.java | 83 --- .../support/CloudFoundrySecurityService.java | 135 ----- .../config/DataFlowServerConfiguration.java | 6 +- .../DataflowOAuthSecurityConfiguration.java | 205 +++++++- .../SkipperOAuthSecurityConfiguration.java | 153 ++++-- 12 files changed, 511 insertions(+), 1061 deletions(-) create mode 100644 spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthClientConfiguration.java delete mode 100644 spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/AccessLevel.java delete mode 100644 spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryAuthorizationException.java delete mode 100644 spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryDataflowAuthoritiesMapper.java delete mode 100644 spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundrySecurityService.java diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java index 702e6dd2db..f3011fff7d 100644 --- a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java +++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/CommonSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; @Configuration(proxyBeanMethods = false) @AutoConfigureBefore({ @@ -29,6 +28,5 @@ ManagementWebSecurityAutoConfiguration.class, OAuth2ClientAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) -@Import({IgnoreAllSecurityConfiguration.class, OAuthSecurityConfiguration.class}) public class CommonSecurityAutoConfiguration { } diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthClientConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthClientConfiguration.java new file mode 100644 index 0000000000..4ef9ba1aa8 --- /dev/null +++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthClientConfiguration.java @@ -0,0 +1,201 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.common.security; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; +import org.springframework.cloud.common.security.support.AuthoritiesMapper; +import org.springframework.cloud.common.security.support.CustomAuthoritiesOpaqueTokenIntrospector; +import org.springframework.cloud.common.security.support.CustomOAuth2OidcUserService; +import org.springframework.cloud.common.security.support.CustomPlainOAuth2UserService; +import org.springframework.cloud.common.security.support.DefaultAuthoritiesMapper; +import org.springframework.cloud.common.security.support.DefaultOAuth2TokenUtilsService; +import org.springframework.cloud.common.security.support.ExternalOauth2ResourceAuthoritiesMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration(proxyBeanMethods = false) +public class OAuthClientConfiguration { + + @Configuration(proxyBeanMethods = false) + protected static class OAuth2AccessTokenResponseClientConfig { + @Bean + OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient() { + return new DefaultPasswordTokenResponseClient(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") + protected static class AuthenticationProviderConfig { + + protected OpaqueTokenIntrospector opaqueTokenIntrospector; + + @Autowired(required = false) + public void setOpaqueTokenIntrospector(OpaqueTokenIntrospector opaqueTokenIntrospector) { + this.opaqueTokenIntrospector = opaqueTokenIntrospector; + } + + @Bean + protected AuthenticationProvider authenticationProvider( + OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient, + ClientRegistrationRepository clientRegistrationRepository, + AuthorizationProperties authorizationProperties, + OAuth2ClientProperties oauth2ClientProperties) { + return new ManualOAuthAuthenticationProvider( + oAuth2PasswordTokenResponseClient, + clientRegistrationRepository, + this.opaqueTokenIntrospector, + calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties)); + + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") + protected static class ProviderManagerConfig { + private AuthenticationProvider authenticationProvider; + + @Autowired(required = false) + protected void setAuthenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProvider = authenticationProvider; + } + + @Bean + protected ProviderManager providerManager() { + List providers = new ArrayList<>(); + providers.add(authenticationProvider); + return new ProviderManager(providers); + } + } + + @Configuration(proxyBeanMethods = false) + protected static class OAuth2TokenUtilsServiceConfig { + @Bean + protected OAuth2TokenUtilsService oauth2TokenUtilsService(OAuth2AuthorizedClientService oauth2AuthorizedClientService) { + return new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService); + } + } + + @Configuration(proxyBeanMethods = false) + protected static class AuthoritiesMapperConfig { + + @Bean + protected AuthoritiesMapper authorityMapper(AuthorizationProperties authorizationProperties, + OAuth2ClientProperties oAuth2ClientProperties) { + AuthoritiesMapper authorityMapper; + if (!StringUtils.hasText(authorizationProperties.getExternalAuthoritiesUrl())) { + authorityMapper = new DefaultAuthoritiesMapper( + authorizationProperties.getProviderRoleMappings(), + calculateDefaultProviderId(authorizationProperties, oAuth2ClientProperties)); + } else { + authorityMapper = new ExternalOauth2ResourceAuthoritiesMapper( + URI.create(authorizationProperties.getExternalAuthoritiesUrl())); + } + return authorityMapper; + } + } + + @Configuration(proxyBeanMethods = false) + protected static class OidcUserServiceConfig { + + @Bean + protected OAuth2UserService oidcUserService(AuthoritiesMapper authoritiesMapper) { + return new CustomOAuth2OidcUserService(authoritiesMapper); + } + } + + @Configuration(proxyBeanMethods = false) + protected static class PlainOauth2UserServiceConfig { + + @Bean + protected OAuth2UserService plainOauth2UserService( + AuthoritiesMapper authoritiesMapper) { + return new CustomPlainOAuth2UserService(authoritiesMapper); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") + protected static class OpaqueTokenIntrospectorConfig { + @Bean + protected OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties oAuth2ResourceServerProperties, + AuthoritiesMapper authoritiesMapper) { + return new CustomAuthoritiesOpaqueTokenIntrospector( + oAuth2ResourceServerProperties.getOpaquetoken().getIntrospectionUri(), + oAuth2ResourceServerProperties.getOpaquetoken().getClientId(), + oAuth2ResourceServerProperties.getOpaquetoken().getClientSecret(), + authoritiesMapper); + } + } + + public static String calculateDefaultProviderId(AuthorizationProperties authorizationProperties, OAuth2ClientProperties oauth2ClientProperties) { + if (authorizationProperties.getDefaultProviderId() != null) { + return authorizationProperties.getDefaultProviderId(); + } + else if (oauth2ClientProperties.getRegistration().size() == 1) { + return oauth2ClientProperties.getRegistration().entrySet().iterator().next() + .getKey(); + } + else if (oauth2ClientProperties.getRegistration().size() > 1 + && !StringUtils.hasText(authorizationProperties.getDefaultProviderId())) { + throw new IllegalStateException("defaultProviderId must be set if more than 1 Registration is provided."); + } + else { + throw new IllegalStateException("Unable to retrieve default provider id."); + } + } + + @Configuration(proxyBeanMethods = false) + protected static class WebClientConfig { + + @Bean + protected WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); + } + } + + +} diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java index fc3227a986..e69de29bb2 100644 --- a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java +++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/OAuthSecurityConfiguration.java @@ -1,495 +0,0 @@ -/* - * Copyright 2016-2022 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.common.security; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.security.SecurityProperties; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; -import org.springframework.cloud.common.security.support.AccessTokenClearingLogoutSuccessHandler; -import org.springframework.cloud.common.security.support.AuthoritiesMapper; -import org.springframework.cloud.common.security.support.CustomAuthoritiesOpaqueTokenIntrospector; -import org.springframework.cloud.common.security.support.CustomOAuth2OidcUserService; -import org.springframework.cloud.common.security.support.CustomPlainOAuth2UserService; -import org.springframework.cloud.common.security.support.DefaultAuthoritiesMapper; -import org.springframework.cloud.common.security.support.DefaultOAuth2TokenUtilsService; -import org.springframework.cloud.common.security.support.ExternalOauth2ResourceAuthoritiesMapper; -import org.springframework.cloud.common.security.support.MappingJwtGrantedAuthoritiesConverter; -import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled; -import org.springframework.cloud.common.security.support.SecurityConfigUtils; -import org.springframework.cloud.common.security.support.SecurityStateBean; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.event.EventListener; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; -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.configurers.ExceptionHandlingConfigurer; -import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AnyRequestMatcher; -import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; -import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.StringUtils; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.accept.HeaderContentNegotiationStrategy; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * Setup Spring Security OAuth for the Rest Endpoints of Spring Cloud Data Flow. - * - * @author Gunnar Hillert - * @author Ilayaperumal Gopinathan - * @author Corneil du Plessis - */ -@Configuration(proxyBeanMethods = false) -// TODO SCDF 3.0 Migration - Need to re add this later with a different class or bean. -// @ConditionalOnClass(WebSecurityConfigurerAdapter.class) -// @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.ANY) -@EnableWebSecurity -@Conditional(OnOAuth2SecurityEnabled.class) -@Import({ - OAuthSecurityConfiguration.OAuth2AccessTokenResponseClientConfig.class, - OAuthSecurityConfiguration.OAuth2AuthenticationFailureEventConfig.class, - OAuthSecurityConfiguration.OpaqueTokenIntrospectorConfig.class, - OAuthSecurityConfiguration.OidcUserServiceConfig.class, - OAuthSecurityConfiguration.PlainOauth2UserServiceConfig.class, - OAuthSecurityConfiguration.WebClientConfig.class, - OAuthSecurityConfiguration.AuthoritiesMapperConfig.class, - OAuthSecurityConfiguration.OAuth2TokenUtilsServiceConfig.class, - OAuthSecurityConfiguration.LogoutSuccessHandlerConfig.class, - OAuthSecurityConfiguration.ProviderManagerConfig.class, - OAuthSecurityConfiguration.AuthenticationProviderConfig.class -}) -public class OAuthSecurityConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(OAuthSecurityConfiguration.class); - - @Autowired - protected OAuth2ClientProperties oauth2ClientProperties; - - @Autowired - protected SecurityStateBean securityStateBean; - - @Autowired - protected SecurityProperties securityProperties; - - @Autowired - protected ApplicationEventPublisher applicationEventPublisher; - - @Autowired - protected AuthorizationProperties authorizationProperties; - - @Autowired - protected OAuth2ResourceServerProperties oAuth2ResourceServerProperties; - - @Autowired - protected OAuth2UserService plainOauth2UserService; - - @Autowired - protected OAuth2UserService oidcUserService; - - @Autowired - protected LogoutSuccessHandler logoutSuccessHandler; - - protected OpaqueTokenIntrospector opaqueTokenIntrospector; - - protected ProviderManager providerManager; - - public AuthorizationProperties getAuthorizationProperties() { - return authorizationProperties; - } - - public void setAuthorizationProperties(AuthorizationProperties authorizationProperties) { - this.authorizationProperties = authorizationProperties; - } - - public OpaqueTokenIntrospector getOpaqueTokenIntrospector() { - return opaqueTokenIntrospector; - } - - @Autowired(required = false) - public void setOpaqueTokenIntrospector(OpaqueTokenIntrospector opaqueTokenIntrospector) { - this.opaqueTokenIntrospector = opaqueTokenIntrospector; - } - - public ProviderManager getProviderManager() { - return providerManager; - } - - @Autowired(required = false) - public void setProviderManager(ProviderManager providerManager) { - this.providerManager = providerManager; - } - - public OAuth2ResourceServerProperties getoAuth2ResourceServerProperties() { - return oAuth2ResourceServerProperties; - } - - public void setoAuth2ResourceServerProperties(OAuth2ResourceServerProperties oAuth2ResourceServerProperties) { - this.oAuth2ResourceServerProperties = oAuth2ResourceServerProperties; - } - - public SecurityStateBean getSecurityStateBean() { - return securityStateBean; - } - - public void setSecurityStateBean(SecurityStateBean securityStateBean) { - this.securityStateBean = securityStateBean; - } - - protected HttpBasicConfigurer configure(HttpSecurity http) throws Exception { - - final RequestMatcher textHtmlMatcher = new MediaTypeRequestMatcher( - new BrowserDetectingContentNegotiationStrategy(), - MediaType.TEXT_HTML); - - final BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint(); - basicAuthenticationEntryPoint.setRealmName(SecurityConfigUtils.BASIC_AUTH_REALM_NAME); - basicAuthenticationEntryPoint.afterPropertiesSet(); - - if (opaqueTokenIntrospector != null) { - BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( - providerManager, basicAuthenticationEntryPoint); - http.addFilter(basicAuthenticationFilter); - } - - this.authorizationProperties.getAuthenticatedPaths().add("/"); - this.authorizationProperties.getAuthenticatedPaths() - .add(dashboard(authorizationProperties, "/**")); - this.authorizationProperties.getAuthenticatedPaths() - .add(this.authorizationProperties.getDashboardUrl()); - this.authorizationProperties.getPermitAllPaths() - .add(this.authorizationProperties.getDashboardUrl()); - this.authorizationProperties.getPermitAllPaths() - .add(dashboard(authorizationProperties, "/**")); - ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry security = - - http.authorizeRequests() - .requestMatchers(this.authorizationProperties.getPermitAllPaths() - .toArray(new String[0])) - .permitAll() - .requestMatchers(this.authorizationProperties.getAuthenticatedPaths() - .toArray(new String[0])) - .authenticated(); - security = SecurityConfigUtils.configureSimpleSecurity(security, this.authorizationProperties); - security.anyRequest().denyAll(); - - - ExceptionHandlingConfigurer configurer = http.httpBasic().and() - .logout() - .logoutSuccessHandler(logoutSuccessHandler) - .and().csrf().disable() - .exceptionHandling() - // for UI not to send basic auth header - .defaultAuthenticationEntryPointFor( - new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")) - .defaultAuthenticationEntryPointFor( - new LoginUrlAuthenticationEntryPoint(this.authorizationProperties.getLoginProcessingUrl()), - textHtmlMatcher) - .defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, AnyRequestMatcher.INSTANCE); - - http.oauth2Login().userInfoEndpoint() - .userService(this.plainOauth2UserService) - .oidcUserService(this.oidcUserService); - - if (opaqueTokenIntrospector != null) { - http.oauth2ResourceServer() - .opaqueToken() - .introspector(opaqueTokenIntrospector); - } - else if (oAuth2ResourceServerProperties.getJwt().getJwkSetUri() != null) { - http.oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); - } - - this.securityStateBean.setAuthenticationEnabled(true); - return http.getConfigurer(HttpBasicConfigurer.class); - } - - protected static String dashboard(AuthorizationProperties authorizationProperties, String path) { - return authorizationProperties.getDashboardUrl() + path; - } - - protected Converter grantedAuthoritiesExtractor() { - String providerId = calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties); - ProviderRoleMapping providerRoleMapping = authorizationProperties.getProviderRoleMappings() - .get(providerId); - - JwtAuthenticationConverter jwtAuthenticationConverter = - new JwtAuthenticationConverter(); - - MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(); - converter.setAuthorityPrefix(""); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); - if (providerRoleMapping != null) { - converter.setAuthoritiesMapping(providerRoleMapping.getRoleMappings()); - converter.setGroupAuthoritiesMapping(providerRoleMapping.getGroupMappings()); - if (StringUtils.hasText(providerRoleMapping.getPrincipalClaimName())) { - jwtAuthenticationConverter.setPrincipalClaimName(providerRoleMapping.getPrincipalClaimName()); - } - } - return jwtAuthenticationConverter; - } - - private static String calculateDefaultProviderId(AuthorizationProperties authorizationProperties, OAuth2ClientProperties oauth2ClientProperties) { - if (authorizationProperties.getDefaultProviderId() != null) { - return authorizationProperties.getDefaultProviderId(); - } - else if (oauth2ClientProperties.getRegistration().size() == 1) { - return oauth2ClientProperties.getRegistration().entrySet().iterator().next() - .getKey(); - } - else if (oauth2ClientProperties.getRegistration().size() > 1 - && !StringUtils.hasText(authorizationProperties.getDefaultProviderId())) { - throw new IllegalStateException("defaultProviderId must be set if more than 1 Registration is provided."); - } - else { - throw new IllegalStateException("Unable to retrieve default provider id."); - } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") - protected static class OpaqueTokenIntrospectorConfig { - @Bean - protected OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties oAuth2ResourceServerProperties, - AuthoritiesMapper authoritiesMapper) { - return new CustomAuthoritiesOpaqueTokenIntrospector( - oAuth2ResourceServerProperties.getOpaquetoken().getIntrospectionUri(), - oAuth2ResourceServerProperties.getOpaquetoken().getClientId(), - oAuth2ResourceServerProperties.getOpaquetoken().getClientSecret(), - authoritiesMapper); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class OidcUserServiceConfig { - @Bean - protected OAuth2UserService oidcUserService(AuthoritiesMapper authoritiesMapper) { - return new CustomOAuth2OidcUserService(authoritiesMapper); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class PlainOauth2UserServiceConfig { - @Bean - protected OAuth2UserService plainOauth2UserService(AuthoritiesMapper authoritiesMapper) { - return new CustomPlainOAuth2UserService(authoritiesMapper); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class OAuth2AuthorizedClientManagerConfig { - @Bean - protected OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - return authorizedClientManager; - } - } - - @Configuration(proxyBeanMethods = false) - protected static class WebClientConfig { - @Bean - protected WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultOAuth2AuthorizedClient(true); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class AuthoritiesMapperConfig { - @Bean - protected AuthoritiesMapper authorityMapper(AuthorizationProperties authorizationProperties, - OAuth2ClientProperties oAuth2ClientProperties) { - AuthoritiesMapper authorityMapper; - if (!StringUtils.hasText(authorizationProperties.getExternalAuthoritiesUrl())) { - authorityMapper = new DefaultAuthoritiesMapper( - authorizationProperties.getProviderRoleMappings(), - calculateDefaultProviderId(authorizationProperties, oAuth2ClientProperties)); - } - else { - authorityMapper = new ExternalOauth2ResourceAuthoritiesMapper( - URI.create(authorizationProperties.getExternalAuthoritiesUrl())); - } - return authorityMapper; - } - } - - @Configuration(proxyBeanMethods = false) - protected static class LogoutSuccessHandlerConfig { - @Bean - protected LogoutSuccessHandler logoutSuccessHandler(AuthorizationProperties authorizationProperties, - OAuth2TokenUtilsService oauth2TokenUtilsService) { - AccessTokenClearingLogoutSuccessHandler logoutSuccessHandler = - new AccessTokenClearingLogoutSuccessHandler(oauth2TokenUtilsService); - logoutSuccessHandler.setDefaultTargetUrl(dashboard(authorizationProperties, "/logout-success-oauth.html")); - return logoutSuccessHandler; - } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") - protected static class AuthenticationProviderConfig { - - protected OpaqueTokenIntrospector opaqueTokenIntrospector; - - @Autowired(required = false) - public void setOpaqueTokenIntrospector(OpaqueTokenIntrospector opaqueTokenIntrospector) { - this.opaqueTokenIntrospector = opaqueTokenIntrospector; - } - - @Bean - protected AuthenticationProvider authenticationProvider( - OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient, - ClientRegistrationRepository clientRegistrationRepository, - AuthorizationProperties authorizationProperties, - OAuth2ClientProperties oauth2ClientProperties) { - return new ManualOAuthAuthenticationProvider( - oAuth2PasswordTokenResponseClient, - clientRegistrationRepository, - this.opaqueTokenIntrospector, - calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties)); - - } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.opaquetoken", value = "introspection-uri") - protected static class ProviderManagerConfig { - private AuthenticationProvider authenticationProvider; - - protected AuthenticationProvider getAuthenticationProvider() { - return authenticationProvider; - } - - @Autowired(required = false) - protected void setAuthenticationProvider(AuthenticationProvider authenticationProvider) { - this.authenticationProvider = authenticationProvider; - } - - @Bean - protected ProviderManager providerManager() { - List providers = new ArrayList<>(); - providers.add(authenticationProvider); - return new ProviderManager(providers); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class OAuth2TokenUtilsServiceConfig { - @Bean - protected OAuth2TokenUtilsService oauth2TokenUtilsService(OAuth2AuthorizedClientService oauth2AuthorizedClientService) { - return new DefaultOAuth2TokenUtilsService(oauth2AuthorizedClientService); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class OAuth2AuthenticationFailureEventConfig { - @EventListener - public void handleOAuth2AuthenticationFailureEvent( - AbstractAuthenticationFailureEvent authenticationFailureEvent) { - logger.warn("An authentication failure event occurred while accessing a REST resource that requires authentication.", - authenticationFailureEvent.getException()); - } - } - - @Configuration(proxyBeanMethods = false) - protected static class OAuth2AccessTokenResponseClientConfig { - @Bean - OAuth2AccessTokenResponseClient oAuth2PasswordTokenResponseClient() { - return new DefaultPasswordTokenResponseClient(); - } - } - - protected static class BrowserDetectingContentNegotiationStrategy extends HeaderContentNegotiationStrategy { - @Override - public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { - final List supportedMediaTypes = super.resolveMediaTypes(request); - final String userAgent = request.getHeader(HttpHeaders.USER_AGENT); - if (userAgent != null && userAgent.contains("Mozilla/5.0") - && !supportedMediaTypes.contains(MediaType.APPLICATION_JSON)) { - return Collections.singletonList(MediaType.TEXT_HTML); - } - return Collections.singletonList(MediaType.APPLICATION_JSON); - } - } -} diff --git a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java index 13e9dfb483..efbaf67abf 100644 --- a/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java +++ b/spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/main/java/org/springframework/cloud/common/security/support/SecurityConfigUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,8 @@ import org.springframework.cloud.common.security.AuthorizationProperties; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -50,13 +51,11 @@ public class SecurityConfigUtils { /** * Read the configuration for "simple" (that is, not ACL based) security and apply it. * - * @param security The ExpressionUrlAuthorizationConfigurer to apply the authorization rules to + * @param auth The Configurer to apply the authorization rules to * @param authorizationProperties Contains the rules to configure authorization - * - * @return ExpressionUrlAuthorizationConfigurer */ - public static ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry configureSimpleSecurity( - ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry security, + public static void configureSimpleSecurity( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry auth, AuthorizationProperties authorizationProperties) { for (String rule : authorizationProperties.getRules()) { Matcher matcher = AUTHORIZATION_RULE.matcher(rule); @@ -69,8 +68,8 @@ public static ExpressionUrlAuthorizationConfigurer.ExpressionInter String attribute = matcher.group(3).trim(); logger.info("Authorization '{}' | '{}' | '{}'", method, attribute, urlPattern); - security = security.requestMatchers(method, urlPattern).access(attribute); + auth.requestMatchers(method, urlPattern).access(new WebExpressionAuthorizationManager(attribute)); } - return security; + } } diff --git a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/CloudFoundryOAuthSecurityConfiguration.java b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/CloudFoundryOAuthSecurityConfiguration.java index 089fba46fc..e69de29bb2 100644 --- a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/CloudFoundryOAuthSecurityConfiguration.java +++ b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/CloudFoundryOAuthSecurityConfiguration.java @@ -1,110 +0,0 @@ -/* - * Copyright 2017-2019 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.dataflow.server.config.cloudfoundry.security; - -import jakarta.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.cloud.common.security.OAuthSecurityConfiguration; -import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; -import org.springframework.cloud.common.security.support.CustomAuthoritiesOpaqueTokenIntrospector; -import org.springframework.cloud.common.security.support.DefaultAuthoritiesMapper; -import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled; -import org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support.CloudFoundryDataflowAuthoritiesMapper; -import org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support.CloudFoundrySecurityService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.web.client.RestTemplate; - -/** - * When running inside Cloud Foundry, this {@link Configuration} class will reconfigure - * Spring Cloud Data Flow's security setup in {@link OAuthSecurityConfiguration}, so that - * only users with the CF_SPACE_DEVELOPER_ROLE} can access the REST APIs. - *

- * Therefore, this configuration will ensure that only Cloud Foundry - * {@code Space Developers} have access to the underlying REST API's. - *

- * For this to happen, a REST call will be made to the Cloud Foundry Permissions API via - * CloudFoundrySecurityService inside the {@link DefaultAuthoritiesMapper}. - *

- * If the user has the respective permissions, the CF_SPACE_DEVELOPER_ROLE will be - * assigned to the user. - *

- * See also: - * https://apidocs.cloudfoundry.org/258/apps/retrieving_permissions_on_a_app.html - * - * @author Gunnar Hillert - * @author Ilayaperumal Gopinathan - */ -@Configuration -@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) -@Conditional(OnOAuth2SecurityEnabled.class) -@Import(CloudFoundryOAuthSecurityConfiguration.CloudFoundryUAAConfiguration.class) -public class CloudFoundryOAuthSecurityConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(CloudFoundryOAuthSecurityConfiguration.class); - - @Autowired - private CustomAuthoritiesOpaqueTokenIntrospector customAuthoritiesOpaqueTokenIntrospector; - - @Autowired(required = false) - private CloudFoundryDataflowAuthoritiesMapper cloudFoundryDataflowAuthoritiesExtractor; - - @PostConstruct - public void init() { - if (this.cloudFoundryDataflowAuthoritiesExtractor != null) { - logger.info("Setting up Cloud Foundry AuthoritiesExtractor for UAA."); - this.customAuthoritiesOpaqueTokenIntrospector.setAuthorityMapper(this.cloudFoundryDataflowAuthoritiesExtractor); - } - } - - @Configuration - @ConditionalOnProperty(name = "spring.cloud.dataflow.security.cf-use-uaa", havingValue = "true") - public class CloudFoundryUAAConfiguration { - - @Value("${vcap.application.cf_api}") - private String cloudControllerUrl; - - @Value("${vcap.application.application_id}") - private String applicationId; - - @Bean - public CloudFoundryDataflowAuthoritiesMapper authoritiesExtractor( - CloudFoundrySecurityService cloudFoundrySecurityService - ) { - return new CloudFoundryDataflowAuthoritiesMapper(cloudFoundrySecurityService); - } - - @Bean - public CloudFoundrySecurityService cloudFoundrySecurityService( - OAuth2TokenUtilsService oauth2TokenUtilsService, - RestTemplate restTemplate) { - return new CloudFoundrySecurityService(oauth2TokenUtilsService, restTemplate, - this.cloudControllerUrl, - this.applicationId); - } - - } - -} diff --git a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/AccessLevel.java b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/AccessLevel.java deleted file mode 100644 index d3bb3e66a6..0000000000 --- a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/AccessLevel.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support; - -import java.util.Arrays; -import java.util.List; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * The specific access level granted to the Cloud Foundry user that's calling the - * endpoints. - * - * @author Madhura Bhave - * @author Gunnar Hillert - */ -public enum AccessLevel { - - /** - * Restricted access to a limited set of endpoints. - */ - RESTRICTED("", "/health", "/info"), - - /** - * No access. - */ - NONE, - - /** - * Full access to all endpoints. - */ - FULL; - - private static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel"; - - private final List endpointPaths; - - AccessLevel(String... endpointPaths) { - this.endpointPaths = Arrays.asList(endpointPaths); - } - - public static AccessLevel get(HttpServletRequest request) { - return (AccessLevel) request.getAttribute(REQUEST_ATTRIBUTE); - } - - /** - * Returns if the access level should allow access to the specified endpoint path. - * @param endpointPath the endpoint path - * @return {@code true} if access is allowed - */ - public boolean isAccessAllowed(String endpointPath) { - return this.endpointPaths.isEmpty() || this.endpointPaths.contains(endpointPath); - } - - public void put(HttpServletRequest request) { - request.setAttribute(REQUEST_ATTRIBUTE, this); - } - -} diff --git a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryAuthorizationException.java b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryAuthorizationException.java deleted file mode 100644 index 33cc327a58..0000000000 --- a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryAuthorizationException.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2017 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support; - -import org.springframework.http.HttpStatus; - -/** - * Authorization exceptions thrown to limit access to the endpoints. - * - * @author Madhura Bhave - */ -public class CloudFoundryAuthorizationException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - private final Reason reason; - - CloudFoundryAuthorizationException(Reason reason, String message) { - this(reason, message, null); - } - - CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) { - super(message); - this.reason = reason; - } - - /** - * Return the status code that should be returned to the client. - * @return the HTTP status code - */ - public HttpStatus getStatusCode() { - return getReason().getStatus(); - } - - /** - * Return the reason why the authorization exception was thrown. - * @return the reason - */ - public Reason getReason() { - return this.reason; - } - - /** - * Reasons why the exception can be thrown. - */ - enum Reason { - - ACCESS_DENIED(HttpStatus.FORBIDDEN), - - INVALID_AUDIENCE(HttpStatus.UNAUTHORIZED), - - INVALID_ISSUER(HttpStatus.UNAUTHORIZED), - - INVALID_KEY_ID(HttpStatus.UNAUTHORIZED), - - INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED), - - INVALID_TOKEN(HttpStatus.UNAUTHORIZED), - - MISSING_AUTHORIZATION(HttpStatus.UNAUTHORIZED), - - TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED), - - UNSUPPORTED_TOKEN_SIGNING_ALGORITHM(HttpStatus.UNAUTHORIZED), - - SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE); - - private final HttpStatus status; - - Reason(HttpStatus status) { - this.status = status; - } - - public HttpStatus getStatus() { - return this.status; - } - } -} diff --git a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryDataflowAuthoritiesMapper.java b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryDataflowAuthoritiesMapper.java deleted file mode 100644 index d0631503b6..0000000000 --- a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundryDataflowAuthoritiesMapper.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017-2019 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.common.security.support.AuthoritiesMapper; -import org.springframework.cloud.common.security.support.CoreSecurityRoles; -import org.springframework.cloud.common.security.support.SecurityConfigUtils; -import org.springframework.security.config.core.GrantedAuthorityDefaults; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.util.StringUtils; - -/** - * This Spring Cloud Data Flow {@link AuthoritiesMapper} will assign all - * {@link CoreSecurityRoles} to the authenticated OAuth2 user IF the user is a "Space - * Developer" in Cloud Foundry. - * - * @author Gunnar Hillert - * - */ -public class CloudFoundryDataflowAuthoritiesMapper implements AuthoritiesMapper { - - private static final Logger logger = LoggerFactory - .getLogger(CloudFoundryDataflowAuthoritiesMapper.class); - - private final CloudFoundrySecurityService cloudFoundrySecurityService; - - public CloudFoundryDataflowAuthoritiesMapper(CloudFoundrySecurityService cloudFoundrySecurityService) { - this.cloudFoundrySecurityService = cloudFoundrySecurityService; - } - - /** - * The returned {@link List} of {@link GrantedAuthority}s contains all roles from - * {@link CoreSecurityRoles}. The roles are prefixed with the value specified in - * {@link GrantedAuthorityDefaults}. - * - * @param providerId Not used - * @param scopes Not used - * @param token Must not be null or empty. - */ - @Override - public Set mapScopesToAuthorities(String providerId, Set scopes, String token) { - if (cloudFoundrySecurityService.isSpaceDeveloper(token)) { - final List rolesAsStrings = new ArrayList<>(); - final Set grantedAuthorities = Stream.of(CoreSecurityRoles.values()) - .map(roleEnum -> { - final String roleName = SecurityConfigUtils.ROLE_PREFIX + roleEnum.getKey(); - rolesAsStrings.add(roleName); - return new SimpleGrantedAuthority(roleName); - }) - .collect(Collectors.toSet()); - logger.info("Adding ALL roles {} to Cloud Foundry Space Developer user.", - StringUtils.collectionToCommaDelimitedString(rolesAsStrings)); - return grantedAuthorities; - } - else { - return Collections.emptySet(); - } - } -} diff --git a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundrySecurityService.java b/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundrySecurityService.java deleted file mode 100644 index 8ffb6a60a0..0000000000 --- a/spring-cloud-dataflow-platform-cloudfoundry/src/main/java/org/springframework/cloud/dataflow/server/config/cloudfoundry/security/support/CloudFoundrySecurityService.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2017-2019 the original author or authors. - * - * 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 - * - * https://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 org.springframework.cloud.dataflow.server.config.cloudfoundry.security.support; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; -import org.springframework.http.HttpStatus; -import org.springframework.http.RequestEntity; -import org.springframework.util.Assert; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.RestTemplate; - -/** - * Cloud Foundry security service to handle REST calls to the cloud controller and UAA. - * - * @author Madhura Bhave - * @author Gunnar Hillert - * @author Ilayaperumal Gopinathan - * - */ -public class CloudFoundrySecurityService { - - private static final Logger logger = LoggerFactory.getLogger(CloudFoundrySecurityService.class); - - private final OAuth2TokenUtilsService oauth2TokenUtilsService; - private final RestTemplate restTemplate; - - private final String cloudControllerUrl; - - private final String applicationId; - - public CloudFoundrySecurityService(OAuth2TokenUtilsService oauth2TokenUtilsService, - RestTemplate restTemplate, String cloudControllerUrl, - String applicationId) { - Assert.notNull(oauth2TokenUtilsService, "oauth2TokenUtilsService must not be null."); - Assert.notNull(restTemplate, "restTemplate must not be null."); - Assert.notNull(cloudControllerUrl, "CloudControllerUrl must not be null."); - Assert.notNull(applicationId, "ApplicationId must not be null."); - this.oauth2TokenUtilsService = oauth2TokenUtilsService; - this.cloudControllerUrl = cloudControllerUrl; - this.applicationId = applicationId; - this.restTemplate = restTemplate; - } - - /** - * Returns {@code true} if the user (using the access-token from the authenticated user) - * has full {@link AccessLevel#FULL} for the provided - * {@code applicationId} - * - * @return true of the user is a space developer in Cloud Foundry - */ - public boolean isSpaceDeveloper() { - final String accessToken = this.oauth2TokenUtilsService.getAccessTokenOfAuthenticatedUser(); - return isSpaceDeveloper(accessToken); - } - - public boolean isSpaceDeveloper(String accessToken) { - Assert.hasText(accessToken, "The accessToken must not be null or empty."); - final AccessLevel accessLevel = getAccessLevel( - accessToken, applicationId); - - if (AccessLevel.FULL.equals(accessLevel)) { - return true; - } - else { - return false; - } - } - - /** - * Return the access level that should be granted to the given token. - * @param token the token - * @param applicationId the cloud foundry application ID - * @return the access level that should be granted - * @throws CloudFoundryAuthorizationException if the token is not authorized - */ - public AccessLevel getAccessLevel(String token, String applicationId) - throws CloudFoundryAuthorizationException { - try { - final URI permissionsUri = getPermissionsUri(applicationId); - logger.info("Using PermissionsUri: " + permissionsUri); - RequestEntity request = RequestEntity.get(permissionsUri) - .header("Authorization", "bearer " + token).build(); - Map body = this.restTemplate.exchange(request, Map.class).getBody(); - if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) { - return AccessLevel.FULL; - } - else { - return AccessLevel.RESTRICTED; - } - } - catch (HttpClientErrorException ex) { - if (ex.getStatusCode().equals(HttpStatus.FORBIDDEN)) { - return AccessLevel.NONE; - } - // TODO GH-2627 - a class of the same name is in boot actuator 2.1. check for differnces - throw new CloudFoundryAuthorizationException(CloudFoundryAuthorizationException.Reason.INVALID_TOKEN, - "Invalid token", ex); - } - catch (HttpServerErrorException ex) { - throw new CloudFoundryAuthorizationException(CloudFoundryAuthorizationException.Reason.SERVICE_UNAVAILABLE, - "Cloud controller not reachable"); - } - } - - private URI getPermissionsUri(String applicationId) { - try { - return new URI(this.cloudControllerUrl + "/v2/apps/" + applicationId - + "/permissions"); - } - catch (URISyntaxException ex) { - throw new IllegalStateException(ex); - } - } -} diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowServerConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowServerConfiguration.java index 91c9699df7..71bb9cf9aa 100644 --- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowServerConfiguration.java +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowServerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,9 @@ WebConfiguration.class, H2ServerConfiguration.class, DataflowTaskExplorerConfiguration.class, - DataFlowTaskConfiguration.class + DataFlowTaskConfiguration.class, + SecurityConfiguration.class + }) @EnableConfigurationProperties({ BatchProperties.class, CommonApplicationProperties.class }) public class DataFlowServerConfiguration { diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java index a27c36e492..8038459f8e 100644 --- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,58 @@ package org.springframework.cloud.dataflow.server.config; -import org.springframework.cloud.common.security.OAuthSecurityConfiguration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.cloud.common.security.AuthorizationProperties; +import org.springframework.cloud.common.security.OAuthClientConfiguration; +import org.springframework.cloud.common.security.ProviderRoleMapping; +import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; +import org.springframework.cloud.common.security.support.AccessTokenClearingLogoutSuccessHandler; +import org.springframework.cloud.common.security.support.MappingJwtGrantedAuthoritiesConverter; import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled; +import org.springframework.cloud.common.security.support.SecurityConfigUtils; +import org.springframework.cloud.common.security.support.SecurityStateBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; +import org.springframework.web.context.request.NativeWebRequest; /** * Setup Spring Security OAuth for the Rest Endpoints of Spring Cloud Data Flow. @@ -32,11 +78,156 @@ */ @Configuration @Conditional(OnOAuth2SecurityEnabled.class) -public class DataflowOAuthSecurityConfiguration extends OAuthSecurityConfiguration { +@Import({ OAuthClientConfiguration.class }) +@EnableWebSecurity +public class DataflowOAuthSecurityConfiguration { + + private final OpaqueTokenIntrospector opaqueTokenIntrospector; + private final AuthenticationManager authenticationManager; + private final AuthorizationProperties authorizationProperties; + private final OAuth2UserService plainOauth2UserService; + private final OAuth2UserService oidcUserService; + private final OAuth2ResourceServerProperties oAuth2ResourceServerProperties; + private final OAuth2ClientProperties oauth2ClientProperties; + private final SecurityStateBean securityStateBean; + private final OAuth2TokenUtilsService oauth2TokenUtilsService; + + public DataflowOAuthSecurityConfiguration(ObjectProvider opaqueTokenIntrospector, + ObjectProvider authenticationManager, + ObjectProvider authorizationProperties, + ObjectProvider> plainOauth2UserService, + ObjectProvider> oidcUserService, + ObjectProvider oAuth2ResourceServerProperties, + ObjectProvider oauth2ClientProperties, + ObjectProvider securityStateBean, + ObjectProvider oauth2TokenUtilsService + ) { + this.opaqueTokenIntrospector = opaqueTokenIntrospector.getIfAvailable(); + this.authenticationManager = authenticationManager.getIfAvailable(); + this.authorizationProperties = authorizationProperties.getIfAvailable(); + this.plainOauth2UserService = plainOauth2UserService.getIfAvailable(); + this.oidcUserService = oidcUserService.getIfAvailable(); + this.oAuth2ResourceServerProperties = oAuth2ResourceServerProperties.getIfAvailable(); + this.oauth2ClientProperties = oauth2ClientProperties.getIfAvailable(); + this.securityStateBean = securityStateBean.getIfAvailable(); + this.oauth2TokenUtilsService = oauth2TokenUtilsService.getIfAvailable(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint(); + basicAuthenticationEntryPoint.setRealmName(SecurityConfigUtils.BASIC_AUTH_REALM_NAME); + basicAuthenticationEntryPoint.afterPropertiesSet(); + + if (opaqueTokenIntrospector != null) { + BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( + authenticationManager, basicAuthenticationEntryPoint); + http.addFilter(basicAuthenticationFilter); + } + + List authenticatedPaths = new ArrayList<>(authorizationProperties.getAuthenticatedPaths()); + authenticatedPaths.add("/"); + authenticatedPaths.add(dashboard(authorizationProperties, "/**")); + authenticatedPaths.add(authorizationProperties.getDashboardUrl()); + + List permitAllPaths = new ArrayList<>(authorizationProperties.getPermitAllPaths()); + permitAllPaths.add(this.authorizationProperties.getDashboardUrl()); + permitAllPaths.add(dashboard(authorizationProperties, "/**")); + + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(permitAllPaths.toArray(new String[0])).permitAll(); + auth.requestMatchers(authenticatedPaths.toArray(new String[0])).authenticated(); + SecurityConfigUtils.configureSimpleSecurity(auth, authorizationProperties); + }); + + http.httpBasic(Customizer.withDefaults()); + + http.logout(auth -> { + auth.logoutSuccessHandler(logoutSuccessHandler(authorizationProperties, oauth2TokenUtilsService)); + }); + + http.csrf(AbstractHttpConfigurer::disable); + + http.exceptionHandling(auth -> { + auth.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + RequestMatcher textHtmlMatcher = new MediaTypeRequestMatcher( + new BrowserDetectingContentNegotiationStrategy(), MediaType.TEXT_HTML); + auth.defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint(this.authorizationProperties.getLoginProcessingUrl()), + textHtmlMatcher); + auth.defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, AnyRequestMatcher.INSTANCE); + }); + + http.oauth2Login(auth -> { + auth.userInfoEndpoint(customizer -> { + customizer.userService(plainOauth2UserService).oidcUserService(oidcUserService); + }); + }); + + http.oauth2ResourceServer(resourceserver -> { + if (opaqueTokenIntrospector != null) { + resourceserver.opaqueToken(opaqueToken -> { + opaqueToken.introspector(opaqueTokenIntrospector); + }); + } + else if (oAuth2ResourceServerProperties.getJwt().getJwkSetUri() != null) { + resourceserver.jwt(jwt -> { + jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()); + }); + } + }); + + securityStateBean.setAuthenticationEnabled(true); + + return http.build(); + } + + private static String dashboard(AuthorizationProperties authorizationProperties, String path) { + return authorizationProperties.getDashboardUrl() + path; + } + + private LogoutSuccessHandler logoutSuccessHandler(AuthorizationProperties authorizationProperties, + OAuth2TokenUtilsService oauth2TokenUtilsService) { + AccessTokenClearingLogoutSuccessHandler logoutSuccessHandler = + new AccessTokenClearingLogoutSuccessHandler(oauth2TokenUtilsService); + logoutSuccessHandler.setDefaultTargetUrl(dashboard(authorizationProperties, "/logout-success-oauth.html")); + return logoutSuccessHandler; + } + + private static class BrowserDetectingContentNegotiationStrategy extends HeaderContentNegotiationStrategy { + @Override + public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { + final List supportedMediaTypes = super.resolveMediaTypes(request); + final String userAgent = request.getHeader(HttpHeaders.USER_AGENT); + if (userAgent != null && userAgent.contains("Mozilla/5.0") + && !supportedMediaTypes.contains(MediaType.APPLICATION_JSON)) { + return Collections.singletonList(MediaType.TEXT_HTML); + } + return Collections.singletonList(MediaType.APPLICATION_JSON); + } + } + + private Converter grantedAuthoritiesExtractor() { + String providerId = OAuthClientConfiguration.calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties); + ProviderRoleMapping providerRoleMapping = authorizationProperties.getProviderRoleMappings() + .get(providerId); + + JwtAuthenticationConverter jwtAuthenticationConverter = + new JwtAuthenticationConverter(); - @Override - protected HttpBasicConfigurer configure(HttpSecurity http) throws Exception { - return super.configure(http); + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(); + converter.setAuthorityPrefix(""); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); + if (providerRoleMapping != null) { + converter.setAuthoritiesMapping(providerRoleMapping.getRoleMappings()); + converter.setGroupAuthoritiesMapping(providerRoleMapping.getGroupMappings()); + if (StringUtils.hasText(providerRoleMapping.getPrincipalClaimName())) { + jwtAuthenticationConverter.setPrincipalClaimName(providerRoleMapping.getPrincipalClaimName()); + } + } + return jwtAuthenticationConverter; } } diff --git a/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/config/security/SkipperOAuthSecurityConfiguration.java b/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/config/security/SkipperOAuthSecurityConfiguration.java index 9c7bc2658f..9444775b5f 100644 --- a/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/config/security/SkipperOAuthSecurityConfiguration.java +++ b/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/config/security/SkipperOAuthSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,35 @@ package org.springframework.cloud.skipper.server.config.security; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.cloud.common.security.AuthorizationProperties; -import org.springframework.cloud.common.security.OAuthSecurityConfiguration; +import org.springframework.cloud.common.security.OAuthClientConfiguration; +import org.springframework.cloud.common.security.ProviderRoleMapping; +import org.springframework.cloud.common.security.support.MappingJwtGrantedAuthoritiesConverter; import org.springframework.cloud.common.security.support.OnOAuth2SecurityEnabled; import org.springframework.cloud.common.security.support.SecurityConfigUtils; import org.springframework.cloud.common.security.support.SecurityStateBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.StringUtils; /** * Setup Spring Security OAuth for the Rest Endpoints of Spring Cloud Data Flow. @@ -40,66 +55,96 @@ */ @Configuration(proxyBeanMethods = false) @Conditional(OnOAuth2SecurityEnabled.class) -public class SkipperOAuthSecurityConfiguration extends OAuthSecurityConfiguration { +@Import({ OAuthClientConfiguration.class }) +@EnableWebSecurity +public class SkipperOAuthSecurityConfiguration { - @Autowired - private SecurityStateBean securityStateBean; + private final OpaqueTokenIntrospector opaqueTokenIntrospector; + private final AuthenticationManager authenticationManager; + private final AuthorizationProperties authorizationProperties; + private final OAuth2ResourceServerProperties oAuth2ResourceServerProperties; + private final OAuth2ClientProperties oauth2ClientProperties; + private final SecurityStateBean securityStateBean; - @Autowired - private AuthorizationProperties authorizationProperties; - - @Override - protected HttpBasicConfigurer configure(HttpSecurity http) throws Exception { + public SkipperOAuthSecurityConfiguration(ObjectProvider opaqueTokenIntrospector, + ObjectProvider authenticationManager, + ObjectProvider authorizationProperties, + ObjectProvider oAuth2ResourceServerProperties, + ObjectProvider oauth2ClientProperties, + ObjectProvider securityStateBean + ){ + this.opaqueTokenIntrospector = opaqueTokenIntrospector.getIfAvailable(); + this.authenticationManager = authenticationManager.getIfAvailable(); + this.authorizationProperties = authorizationProperties.getIfAvailable(); + this.oAuth2ResourceServerProperties = oAuth2ResourceServerProperties.getIfAvailable(); + this.oauth2ClientProperties = oauth2ClientProperties.getIfAvailable(); + this.securityStateBean = securityStateBean.getIfAvailable(); + } - final BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint(); + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint(); basicAuthenticationEntryPoint.setRealmName(SecurityConfigUtils.BASIC_AUTH_REALM_NAME); basicAuthenticationEntryPoint.afterPropertiesSet(); if (opaqueTokenIntrospector != null) { BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( - getProviderManager(), - basicAuthenticationEntryPoint - ); + authenticationManager, basicAuthenticationEntryPoint); http.addFilter(basicAuthenticationFilter); } - getAuthorizationProperties().getAuthenticatedPaths() - .add(dashboard(getAuthorizationProperties(), "/**")); - getAuthorizationProperties().getAuthenticatedPaths() - .add(dashboard(getAuthorizationProperties(), "")); - - ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry security = - http.authorizeRequests() - .requestMatchers(getAuthorizationProperties().getPermitAllPaths() - .toArray(new String[0])) - .permitAll() - .requestMatchers(getAuthorizationProperties().getAuthenticatedPaths() - .toArray(new String[0])) - .authenticated(); - - security = SecurityConfigUtils.configureSimpleSecurity(security, getAuthorizationProperties()); - security.anyRequest().denyAll(); - - http.httpBasic().and() - .logout() - .logoutSuccessUrl(dashboard(getAuthorizationProperties(), "/logout-success-oauth.html")) - .and().csrf().disable() - .exceptionHandling() - .defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, new AntPathRequestMatcher("/api/**")) - .defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, new AntPathRequestMatcher("/actuator/**")); - - if (getOpaqueTokenIntrospector() != null) { - http.oauth2ResourceServer() - .opaqueToken() - .introspector(getOpaqueTokenIntrospector()); - } - else if (getoAuth2ResourceServerProperties().getJwt().getJwkSetUri() != null) { - http.oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); - } + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(authorizationProperties.getPermitAllPaths().toArray(String[]::new)).permitAll(); + auth.requestMatchers(authorizationProperties.getAuthenticatedPaths().toArray(String[]::new)).authenticated(); + SecurityConfigUtils.configureSimpleSecurity(auth, authorizationProperties); + }); + + + http.httpBasic(Customizer.withDefaults()); + http.csrf(AbstractHttpConfigurer::disable); + + http.exceptionHandling(auth -> { + auth.defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, new AntPathRequestMatcher("/api/**")); + auth.defaultAuthenticationEntryPointFor(basicAuthenticationEntryPoint, new AntPathRequestMatcher("/actuator/**")); + }); + + http.oauth2ResourceServer(resourceserver -> { + if (opaqueTokenIntrospector != null) { + resourceserver.opaqueToken(opaqueToken -> { + opaqueToken.introspector(opaqueTokenIntrospector); + }); + } + else if (oAuth2ResourceServerProperties.getJwt().getJwkSetUri() != null) { + resourceserver.jwt(jwt -> { + jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()); + }); + } + }); - getSecurityStateBean().setAuthenticationEnabled(true); - return http.getConfigurer(HttpBasicConfigurer.class); + securityStateBean.setAuthenticationEnabled(true); + + return http.build(); } + + private Converter grantedAuthoritiesExtractor() { + String providerId = OAuthClientConfiguration.calculateDefaultProviderId(authorizationProperties, oauth2ClientProperties); + ProviderRoleMapping providerRoleMapping = authorizationProperties.getProviderRoleMappings() + .get(providerId); + + JwtAuthenticationConverter jwtAuthenticationConverter = + new JwtAuthenticationConverter(); + + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(); + converter.setAuthorityPrefix(""); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); + if (providerRoleMapping != null) { + converter.setAuthoritiesMapping(providerRoleMapping.getRoleMappings()); + converter.setGroupAuthoritiesMapping(providerRoleMapping.getGroupMappings()); + if (StringUtils.hasText(providerRoleMapping.getPrincipalClaimName())) { + jwtAuthenticationConverter.setPrincipalClaimName(providerRoleMapping.getPrincipalClaimName()); + } + } + return jwtAuthenticationConverter; + } + }