Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
import lombok.AllArgsConstructor;
import org.opendevstack.component_provisioner.config.azure.ConditionalAadFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
Expand All @@ -16,6 +17,8 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfiguration;

@Configuration
Expand Down Expand Up @@ -65,38 +68,39 @@ SecurityFilterChain basicForProvisionDelete(HttpSecurity http) throws Exception
@Bean
@Order(2)
public SecurityFilterChain aadForEverythingElse(HttpSecurity http) throws Exception {
RequestMatcher protectedEndpoints = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher("/v1/**"),
PathPatternRequestMatcher.withDefaults().matcher("/actuator/**")
);

RequestMatcher whitelistedEndpoints = new OrRequestMatcher(
PathPatternRequestMatcher.withDefaults().matcher("/api-docs/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v3/api-docs/**"),
PathPatternRequestMatcher.withDefaults().matcher("/actuator/health"),
PathPatternRequestMatcher.withDefaults().matcher("/actuator/mappings"),
PathPatternRequestMatcher.withDefaults().matcher("/v1/message-definitions/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v1/catalog-items/*/message-definitions/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v1/catalog-items/*/user-actions/*/message-definitions/**")
);


http
.authorizeHttpRequests(request -> request
.requestMatchers(
"/api-docs/**",
"/v3/api-docs/**"
)
.permitAll()
.authorizeHttpRequests(req -> req
.requestMatchers(
"/v1/message-definitions/**",
"/v1/catalog-items/*/message-definitions/**",
"/v1/catalog-items/*/user-actions/*/message-definitions/**"
)
.permitAll()
.requestMatchers(
V_1_PROVISION
)
.permitAll()
.requestMatchers("/actuator/health")
.permitAll()

.requestMatchers("/v1/**", "/actuator/**")
.hasAuthority("ROLE_USER") // If required, change or add proper roles set by AAD
whitelistedEndpoints
).permitAll()
.anyRequest().hasAuthority("ROLE_USER")
)
.csrf(CsrfConfigurer::disable) //NOSONAR required for /actuator endpoints, STATELESS prevents CSRF
.cors(c -> c.configurationSource(request ->
new CorsConfiguration().applyPermitDefaultValues())) //NOSONAR required for CORS support for browser requests
.sessionManagement(configurer ->
// Avoid session caching and validation e.g. via JSESSIONID cookie, as we are stateless
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 2) Azure AD (bearer/JWT) for the rest
.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(
new ConditionalAadFilter(aadAuthFilter, protectedEndpoints, whitelistedEndpoints),
UsernamePasswordAuthenticationFilter.class
);

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.opendevstack.component_provisioner.config.azure;

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
public class ConditionalAadFilter extends OncePerRequestFilter {

// Ideally, we should create different filters and order them in the security configuration.
// The thing is, AadAppRoleStatelessAuthenticationFilter is not ordered, so if we try to do so, spring will complain.
// The solution would be, wrap it and add an @order annotation, or wrap it in the conditional filter itself, as we do here.
private final AadAppRoleStatelessAuthenticationFilter delegate;
private final RequestMatcher protectedEndpoints;
private final RequestMatcher whitelistedEndpoints;

public ConditionalAadFilter(
AadAppRoleStatelessAuthenticationFilter delegate,
RequestMatcher protectedEndpoints,
RequestMatcher whitelistedEndpoints) {
this.delegate = delegate;
this.protectedEndpoints = protectedEndpoints;
this.whitelistedEndpoints = whitelistedEndpoints;
}

@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
var shouldNotFilter = whitelistedEndpoints.matches(request) || !protectedEndpoints.matches(request);

log.debug("Validating url {}: protectedEndpoints matches? {}, whitelistedEndpoints matches? {}, shouldNotFilter? {}",
request.getRequestURI(),
protectedEndpoints.matches(request),
whitelistedEndpoints.matches(request),
shouldNotFilter);

return shouldNotFilter;
}

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws IOException, ServletException {

delegate.doFilter(request, response, filterChain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.opendevstack.component_provisioner.config.azure;

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.security.web.util.matcher.RequestMatcher;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

class ConditionalAadFilterTest {

private AadAppRoleStatelessAuthenticationFilter delegate;
private RequestMatcher protectedEndpoints;
private RequestMatcher whitelistedEndpoints;

private ConditionalAadFilter filter;

@BeforeEach
void setup() {
delegate = mock(AadAppRoleStatelessAuthenticationFilter.class);
protectedEndpoints = mock(RequestMatcher.class);
whitelistedEndpoints = mock(RequestMatcher.class);

filter = new ConditionalAadFilter(delegate, protectedEndpoints, whitelistedEndpoints);
}

@Test
void givenWhitelistedPath_whenShouldNotFilter_thenTrue() {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
when(whitelistedEndpoints.matches(request)).thenReturn(true);

// when
boolean result = filter.shouldNotFilter(request);

// then
assertThat(result).isTrue();
}

@Test
void givenProtectedAndNotWhitelisted_whenShouldNotFilter_thenFalse() {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
when(whitelistedEndpoints.matches(request)).thenReturn(false);
when(protectedEndpoints.matches(request)).thenReturn(true);

// when
boolean result = filter.shouldNotFilter(request);

// then
assertThat(result).isFalse();
}

@Test
void givenNotProtectedAndNotWhitelisted_whenShouldNotFilter_thenTrue() {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
when(whitelistedEndpoints.matches(request)).thenReturn(false);
when(protectedEndpoints.matches(request)).thenReturn(false);

// when
boolean result = filter.shouldNotFilter(request);

// then
assertThat(result).isTrue();
}

@Test
void givenAnyRequest_whenDoFilterInternal_thenDelegateIsCalled() throws Exception {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);

// when
filter.doFilterInternal(request, response, filterChain);

// then
verify(delegate).doFilter(request, response, filterChain);
}
}
Loading