From b0ffca7f67ce6b619e47fd2f007fba7fe4dc2696 Mon Sep 17 00:00:00 2001 From: Vladysl <45620393+Vladysl@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:13:45 +0200 Subject: [PATCH] 1639 - added s2s token support (#1646) --- .../oddplatform/auth/S2sTokenProvider.java | 29 +++++++++++ .../auth/filter/S2sAuthenticationFilter.java | 49 +++++++++++++++++++ .../config/LDAPSecurityConfiguration.java | 18 +++++-- .../LoginFormSecurityConfiguration.java | 19 +++++-- .../config/OAuthSecurityConfiguration.java | 10 +++- .../config/WebClientConfiguration.java | 16 +++--- .../config/properties/GenAIProperties.java | 12 +++++ .../service/genai/GenAIServiceImpl.java | 16 +++--- .../src/main/resources/application.yml | 8 ++- 9 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/S2sTokenProvider.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/filter/S2sAuthenticationFilter.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/properties/GenAIProperties.java diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/S2sTokenProvider.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/S2sTokenProvider.java new file mode 100644 index 000000000..c37e10d75 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/S2sTokenProvider.java @@ -0,0 +1,29 @@ +package org.opendatadiscovery.oddplatform.auth; + +import jakarta.annotation.PostConstruct; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class S2sTokenProvider { + @Value("${auth.s2s.token:#{null}}") + private String s2sToken; + @Value("${auth.s2s.enabled:false}") + private boolean s2sEnabled; + + public boolean isValidToken(final String token) { + if (StringUtils.isBlank(token)) { + return false; + } + + return s2sToken.equals(token); + } + + @PostConstruct + public void validate() { + if (s2sEnabled && StringUtils.isBlank(s2sToken)) { + throw new IllegalStateException("Long Term Token is not defined"); + } + } +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/filter/S2sAuthenticationFilter.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/filter/S2sAuthenticationFilter.java new file mode 100644 index 000000000..15b223aea --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/auth/filter/S2sAuthenticationFilter.java @@ -0,0 +1,49 @@ +package org.opendatadiscovery.oddplatform.auth.filter; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.oddplatform.auth.S2sTokenProvider; +import org.opendatadiscovery.oddplatform.auth.mapper.GrantedAuthorityExtractor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +@Component +@RequiredArgsConstructor +public class S2sAuthenticationFilter implements WebFilter { + private static final String X_API_KEY_HEADER = "X-API-Key"; + + private final GrantedAuthorityExtractor grantedAuthorityExtractor; + private final S2sTokenProvider s2sTokenProvider; + + @Override + public Mono filter(final ServerWebExchange exchange, final WebFilterChain chain) { + if (!s2sTokenProvider.isValidToken(extractTokenFromRequest(exchange))) { + return chain.filter(exchange); + } + + final UserDetails userDetails = User.withUsername("ADMIN") + .password("") + .roles("ADMIN") + .build(); + + return chain.filter(exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication( + new UsernamePasswordAuthenticationToken(userDetails, null, + grantedAuthorityExtractor.getAuthorities(true)))); + } + + private String extractTokenFromRequest(final ServerWebExchange exchange) { + final List authorizationHeaders = exchange.getRequest().getHeaders().get(X_API_KEY_HEADER); + if (authorizationHeaders != null && !authorizationHeaders.isEmpty()) { + return authorizationHeaders.get(0); + } + return null; + } +} \ No newline at end of file diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LDAPSecurityConfiguration.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LDAPSecurityConfiguration.java index f3cee6e88..d0a9e2ca2 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LDAPSecurityConfiguration.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LDAPSecurityConfiguration.java @@ -8,9 +8,11 @@ import org.apache.commons.lang3.StringUtils; import org.opendatadiscovery.oddplatform.auth.ODDLDAPProperties; import org.opendatadiscovery.oddplatform.auth.authorization.AuthorizationCustomizer; +import org.opendatadiscovery.oddplatform.auth.filter.S2sAuthenticationFilter; import org.opendatadiscovery.oddplatform.auth.manager.extractor.ResourceExtractor; import org.opendatadiscovery.oddplatform.auth.mapper.GrantedAuthorityExtractor; import org.opendatadiscovery.oddplatform.service.permission.PermissionService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -27,6 +29,7 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; @@ -53,6 +56,7 @@ @Slf4j public class LDAPSecurityConfiguration { private final ODDLDAPProperties properties; + private final S2sAuthenticationFilter s2sAuthenticationFilter; @Bean public ReactiveAuthenticationManager authenticationManager(final LdapContextSource contextSource, @@ -132,15 +136,21 @@ public LdapTemplate ldapTemplate(final ContextSource contextSource) { @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityWebFilterChain configureLdap(final ServerHttpSecurity http, final List extractors, - final PermissionService permissionService) { - return http + final PermissionService permissionService, + @Value("${auth.s2s.enabled:false}") final boolean s2sEnabled) { + final ServerHttpSecurity sec = http .cors(Customizer.withDefaults()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/**")) .authorizeExchange(new AuthorizationCustomizer(permissionService, extractors)) .logout(Customizer.withDefaults()) - .formLogin(Customizer.withDefaults()) - .build(); + .formLogin(Customizer.withDefaults()); + + if (s2sEnabled) { + sec.addFilterAt(s2sAuthenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC); + } + + return sec.build(); } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LoginFormSecurityConfiguration.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LoginFormSecurityConfiguration.java index b8be59624..a5855e88b 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LoginFormSecurityConfiguration.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/LoginFormSecurityConfiguration.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.oddplatform.auth.filter.S2sAuthenticationFilter; import org.opendatadiscovery.oddplatform.auth.mapper.GrantedAuthorityExtractor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -13,6 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; @@ -31,12 +33,13 @@ @RequiredArgsConstructor public class LoginFormSecurityConfiguration { private final GrantedAuthorityExtractor grantedAuthorityExtractor; + private final S2sAuthenticationFilter s2sAuthenticationFilter; @Bean public SecurityWebFilterChain securityWebFilterChainLoginForm( final ServerHttpSecurity http, - @Value("${auth.login-form-redirect:}") final String redirectURIString - ) { + @Value("${auth.login-form-redirect:}") final String redirectURIString, + @Value("${auth.s2s.enabled:false}") final boolean s2sEnabled) { final URI redirectURI = parseURI(redirectURIString); final ServerAuthenticationSuccessHandler authHandler = redirectURI != null @@ -46,14 +49,20 @@ public SecurityWebFilterChain securityWebFilterChainLoginForm( final String[] permittedPaths = new String[] { "/actuator/health", "/favicon.ico", "/ingestion/entities", "/api/slack/events" }; - return http + + final ServerHttpSecurity sec = http .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec .pathMatchers(permittedPaths).permitAll() .pathMatchers("/**").authenticated()) .formLogin(formLoginSpec -> formLoginSpec.authenticationSuccessHandler(authHandler)) - .logout(Customizer.withDefaults()) - .build(); + .logout(Customizer.withDefaults()); + + if (s2sEnabled) { + sec.addFilterAt(s2sAuthenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC); + } + + return sec.build(); } @Bean diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/OAuthSecurityConfiguration.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/OAuthSecurityConfiguration.java index 97c753b17..c250f3841 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/OAuthSecurityConfiguration.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/OAuthSecurityConfiguration.java @@ -15,11 +15,13 @@ import org.opendatadiscovery.oddplatform.auth.ODDOAuth2PropertiesConverter; import org.opendatadiscovery.oddplatform.auth.Provider; import org.opendatadiscovery.oddplatform.auth.authorization.AuthorizationCustomizer; +import org.opendatadiscovery.oddplatform.auth.filter.S2sAuthenticationFilter; import org.opendatadiscovery.oddplatform.auth.handler.OAuthUserHandler; import org.opendatadiscovery.oddplatform.auth.logout.OAuthLogoutSuccessHandler; import org.opendatadiscovery.oddplatform.auth.manager.extractor.ResourceExtractor; import org.opendatadiscovery.oddplatform.dto.security.UserProviderRole; import org.opendatadiscovery.oddplatform.service.permission.PermissionService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper; @@ -73,6 +75,7 @@ @RequiredArgsConstructor public class OAuthSecurityConfiguration { private final ODDOAuth2Properties properties; + private final S2sAuthenticationFilter s2sAuthenticationFilter; private final List> oauthUserHandlers; private final List> oidcUserHandlers; @@ -83,7 +86,8 @@ public SecurityWebFilterChain securityWebFilterChainOauth2Client( final ReactiveClientRegistrationRepository repo, final PermissionService permissionService, final List extractors, - final TemplateEngine templateEngine) { + final TemplateEngine templateEngine, + @Value("${auth.s2s.enabled:false}") final boolean s2sEnabled) { final List clientRegistrations = IteratorUtils.toList(((InMemoryReactiveClientRegistrationRepository) repo).iterator()); @@ -101,6 +105,10 @@ public SecurityWebFilterChain securityWebFilterChainOauth2Client( SecurityWebFiltersOrder.CSRF); } + if (s2sEnabled) { + sec.addFilterAt(s2sAuthenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC); + } + return sec.build(); } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java index 13c88413c..74cacaece 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java @@ -1,7 +1,9 @@ package org.opendatadiscovery.oddplatform.config; import java.time.Duration; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.oddplatform.config.properties.GenAIProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.reactive.ClientHttpConnector; @@ -10,20 +12,20 @@ import reactor.netty.http.client.HttpClient; @Configuration +@EnableConfigurationProperties(GenAIProperties.class) +@RequiredArgsConstructor public class WebClientConfiguration { - @Value("${genai.url:}") - private String genAIUrl; - @Value("${genai.request_timeout:2}") - private Integer getAiRequestTimeout; + private final GenAIProperties genAIProperties; @Bean("genAiWebClient") public WebClient webClient() { - final HttpClient httpClient = HttpClient.create().responseTimeout(Duration.ofMinutes(getAiRequestTimeout)); + final HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMinutes(genAIProperties.getRequestTimeout())); final ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); return WebClient.builder() .clientConnector(connector) - .baseUrl(genAIUrl) + .baseUrl(genAIProperties.getUrl()) .build(); } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/properties/GenAIProperties.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/properties/GenAIProperties.java new file mode 100644 index 000000000..7d589c655 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/properties/GenAIProperties.java @@ -0,0 +1,12 @@ +package org.opendatadiscovery.oddplatform.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("genai") +@Data +public class GenAIProperties { + private boolean enabled; + private String url; + private int requestTimeout; +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java index 37b384089..4cdd9ddc6 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java @@ -4,15 +4,14 @@ import io.netty.handler.timeout.ReadTimeoutException; import java.util.Map; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.opendatadiscovery.oddplatform.api.contract.model.GenAIRequest; import org.opendatadiscovery.oddplatform.api.contract.model.GenAIResponse; +import org.opendatadiscovery.oddplatform.config.properties.GenAIProperties; import org.opendatadiscovery.oddplatform.exception.BadUserRequestException; import org.opendatadiscovery.oddplatform.exception.GenAIException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -23,22 +22,19 @@ public class GenAIServiceImpl implements GenAIService { public static final String QUERY_DATA = "/query_data"; public static final String QUESTION_FIELD = "question"; - private final String genAIUrl; - private final Integer getAiRequestTimeout; + private final GenAIProperties genAIProperties; private final WebClient webClient; @Autowired - public GenAIServiceImpl(@Value("${genai.url:}") final String genAIUrl, - @Value("${genai.request_timeout:2}") final Integer getAiRequestTimeout, + public GenAIServiceImpl(final GenAIProperties genAIProperties, @Qualifier("genAiWebClient") final WebClient webClient) { - this.genAIUrl = genAIUrl; - this.getAiRequestTimeout = getAiRequestTimeout; + this.genAIProperties = genAIProperties; this.webClient = webClient; } @Override public Mono getResponseFromGenAI(final GenAIRequest request) { - if (StringUtils.isBlank(genAIUrl)) { + if (!genAIProperties.isEnabled()) { return Mono.error(new BadUserRequestException("Gen AI is disabled")); } @@ -51,7 +47,7 @@ public Mono getResponseFromGenAI(final GenAIRequest request) { .body(StringEscapeUtils.unescapeJava(CharMatcher.is('\"').trimFrom(item)))) .onErrorResume(e -> e.getCause() instanceof ReadTimeoutException ? Mono.error(new GenAIException( - "Gen AI request take longer that %s min".formatted(getAiRequestTimeout))) + "Gen AI request take longer that %s min".formatted(genAIProperties.getRequestTimeout()))) : Mono.error(new GenAIException(e))); } } diff --git a/odd-platform-api/src/main/resources/application.yml b/odd-platform-api/src/main/resources/application.yml index be01cf54d..717a2de44 100644 --- a/odd-platform-api/src/main/resources/application.yml +++ b/odd-platform-api/src/main/resources/application.yml @@ -14,7 +14,8 @@ spring: codec: max-in-memory-size: 20MB -#genai: +genai: + enabled: false # url: http://localhost:5000 # request_timeout: 2 @@ -35,6 +36,11 @@ auth: # For dev/demo purposes only -- username1:password1,username2:password2,etc login-form-credentials: admin:admin,root:root + # Server-To-Server token. Header 'X-API-Key: token' + s2s: + enabled: false +# token: + # For dev purposes only -- successful auth redirect URI login-form-redirect: ingestion: