Skip to content

Commit

Permalink
1639 - added s2s token support (#1646)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladysl committed Mar 4, 2024
1 parent 5887dbd commit b0ffca7
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Void> 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<String> authorizationHeaders = exchange.getRequest().getHeaders().get(X_API_KEY_HEADER);
if (authorizationHeaders != null && !authorizationHeaders.isEmpty()) {
return authorizationHeaders.get(0);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -53,6 +56,7 @@
@Slf4j
public class LDAPSecurityConfiguration {
private final ODDLDAPProperties properties;
private final S2sAuthenticationFilter s2sAuthenticationFilter;

@Bean
public ReactiveAuthenticationManager authenticationManager(final LdapContextSource contextSource,
Expand Down Expand Up @@ -132,15 +136,21 @@ public LdapTemplate ldapTemplate(final ContextSource contextSource) {
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityWebFilterChain configureLdap(final ServerHttpSecurity http,
final List<ResourceExtractor> 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();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
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;
import org.springframework.context.annotation.Bean;
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;
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +75,7 @@
@RequiredArgsConstructor
public class OAuthSecurityConfiguration {
private final ODDOAuth2Properties properties;
private final S2sAuthenticationFilter s2sAuthenticationFilter;
private final List<OAuthUserHandler<OAuth2User, OAuth2UserRequest>> oauthUserHandlers;
private final List<OAuthUserHandler<OidcUser, OidcUserRequest>> oidcUserHandlers;

Expand All @@ -83,7 +86,8 @@ public SecurityWebFilterChain securityWebFilterChainOauth2Client(
final ReactiveClientRegistrationRepository repo,
final PermissionService permissionService,
final List<ResourceExtractor> extractors,
final TemplateEngine templateEngine) {
final TemplateEngine templateEngine,
@Value("${auth.s2s.enabled:false}") final boolean s2sEnabled) {
final List<ClientRegistration> clientRegistrations =
IteratorUtils.toList(((InMemoryReactiveClientRegistrationRepository) repo).iterator());

Expand All @@ -101,6 +105,10 @@ public SecurityWebFilterChain securityWebFilterChainOauth2Client(
SecurityWebFiltersOrder.CSRF);
}

if (s2sEnabled) {
sec.addFilterAt(s2sAuthenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
}

return sec.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<GenAIResponse> getResponseFromGenAI(final GenAIRequest request) {
if (StringUtils.isBlank(genAIUrl)) {
if (!genAIProperties.isEnabled()) {
return Mono.error(new BadUserRequestException("Gen AI is disabled"));
}

Expand All @@ -51,7 +47,7 @@ public Mono<GenAIResponse> 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)));
}
}
8 changes: 7 additions & 1 deletion odd-platform-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ spring:
codec:
max-in-memory-size: 20MB

#genai:
genai:
enabled: false
# url: http://localhost:5000
# request_timeout: 2

Expand All @@ -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:
Expand Down

0 comments on commit b0ffca7

Please sign in to comment.