Skip to content

Commit

Permalink
fix(SILVA-514 + sec): fixing favorite name on search result and secur…
Browse files Browse the repository at this point in the history
…ity update (#491)
  • Loading branch information
paulushcgcj authored Nov 21, 2024
1 parent 19f27b2 commit 9f26ea8
Show file tree
Hide file tree
Showing 38 changed files with 698 additions and 711 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
java-distribution: temurin
java-version: 21
sonar_args: >
-Dsonar.exclusions=**/configuration/**,**/dto/**,**/entity/**,**/exception/**,**/job/**,**/*$*Builder*,**/ResultsApplication.*,**/*Constants.*,
-Dsonar.exclusions=**/configuration/**,**/dto/**,**/entity/**,**/exception/**,**/job/**,**/*$*Builder*,**/ResultsApplication.*,**/*Constants.*,**/security/*Converter.*
-Dsonar.coverage.jacoco.xmlReportPaths=target/coverage-reports/merged-test-report/jacoco.xml
-Dsonar.organization=bcgov-sonarcloud
-Dsonar.project.monorepo.enabled=true
Expand Down
2 changes: 2 additions & 0 deletions backend/openshift.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ objects:
secretKeyRef:
name: ${NAME}-${ZONE}-database
key: database-user
- name: SELF_URI
value: https://${NAME}-${ZONE}-${COMPONENT}.${DOMAIN}
- name: RANDOM_EXPRESSION
value: ${RANDOM_EXPRESSION}
resources:
Expand Down
1 change: 1 addition & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@
<exclude>**/*$*Builder*</exclude>
<exclude>**/ResultsApplication.*</exclude>
<exclude>**/*Constants.*</exclude>
<exclude>**/security/*Converter.*</exclude>
</excludes>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
package ca.bc.gov.restapi.results.common.configuration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/** This class holds the configuration for CORS handling. */
/**
* This class holds the configuration for CORS handling.
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class CorsConfiguration implements WebMvcConfigurer {

@Value("${server.allowed.cors.origins}")
private String[] allowedOrigins;
private final SilvaConfiguration configuration;

/**
* Adds CORS mappings and allowed origins.
*
* @param registry Spring Cors Registry
*/
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
if (allowedOrigins != null && allowedOrigins.length != 0) {
log.info("allowedOrigins: {}", Arrays.asList(allowedOrigins));
var frontendConfig = configuration.getFrontend();
var cors = frontendConfig.getCors();
String origins = frontendConfig.getUrl();
List<String> allowedOrigins = new ArrayList<>();

registry
.addMapping("/**")
.allowedOriginPatterns(allowedOrigins)
.allowedMethods("GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS", "HEAD");
if (StringUtils.isNotBlank(origins) && origins.contains(",")) {
allowedOrigins.addAll(Arrays.asList(origins.split(",")));
} else {
allowedOrigins.add(origins);
}

log.info("Allowed origins: {} {}", allowedOrigins,allowedOrigins.toArray(new String[0]));

registry
.addMapping("/api/**")
.allowedOriginPatterns(allowedOrigins.toArray(new String[0]))
.allowedMethods(cors.getMethods().toArray(new String[0]))
.allowedHeaders(cors.getHeaders().toArray(new String[0]))
.exposedHeaders(cors.getHeaders().toArray(new String[0]))
.maxAge(cors.getAge().getSeconds())
.allowCredentials(true);

registry.addMapping("/actuator/**")
.allowedOrigins("*")
.allowedMethods("GET")
.allowedHeaders("*")
.allowCredentials(false);

WebMvcConfigurer.super.addCorsMappings(registry);
}
}
Original file line number Diff line number Diff line change
@@ -1,88 +1,41 @@
package ca.bc.gov.restapi.results.common.configuration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import ca.bc.gov.restapi.results.common.security.ApiAuthorizationCustomizer;
import ca.bc.gov.restapi.results.common.security.CsrfSecurityCustomizer;
import ca.bc.gov.restapi.results.common.security.HeadersSecurityCustomizer;
import ca.bc.gov.restapi.results.common.security.Oauth2SecurityCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

/** This class contains all configurations related to security and authentication. */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
String jwkSetUri;

/**
* Filters a request to add security checks and configurations.
*
* @param http instance of HttpSecurity containing the request.
* @return SecurityFilterChain with allowed endpoints and all configuration.
* @throws Exception due to bad configuration possibilities.
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(Customizer.withDefaults())
.csrf(
customize ->
customize.csrfTokenRepository(new CookieCsrfTokenRepository()))
.authorizeHttpRequests(
customize ->
customize
.requestMatchers("/api/**")
.authenticated()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.anyRequest()
.permitAll())
public SecurityFilterChain filterChain(
HttpSecurity http,
HeadersSecurityCustomizer headersCustomizer,
CsrfSecurityCustomizer csrfCustomizer,
ApiAuthorizationCustomizer apiCustomizer,
Oauth2SecurityCustomizer oauth2Customizer
) throws Exception {
http
.headers(headersCustomizer)
.csrf(csrfCustomizer)
.cors(Customizer.withDefaults())
.authorizeHttpRequests(apiCustomizer)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.oauth2ResourceServer(
customize ->
customize.jwt(
jwt -> jwt.jwtAuthenticationConverter(converter()).jwkSetUri(jwkSetUri)));
.oauth2ResourceServer(oauth2Customizer);

return http.build();
}

private Converter<Jwt, AbstractAuthenticationToken> converter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(roleConverter);
return converter;
}

private final Converter<Jwt, Collection<GrantedAuthority>> roleConverter =
jwt -> {
if (!jwt.getClaims().containsKey("client_roles")) {
return List.of();
}
Object clientRolesObj = jwt.getClaims().get("client_roles");
final List<String> realmAccess = new ArrayList<>();
if (clientRolesObj instanceof List<?> list) {
for (Object item : list) {
realmAccess.add(String.valueOf(item));
}
}
return realmAccess.stream()
.map(roleName -> "ROLE_" + roleName)
.map(roleName -> (GrantedAuthority) new SimpleGrantedAuthority(roleName))
.toList();
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.bc.gov.restapi.results.common.configuration;

import java.time.Duration;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -34,6 +35,8 @@ public class SilvaConfiguration {
private ExternalApiAddress openMaps;
@NestedConfigurationProperty
private SilvaDataLimits limits;
@NestedConfigurationProperty
private FrontEndConfiguration frontend;

@Data
@Builder
Expand All @@ -52,4 +55,33 @@ public static class SilvaDataLimits {
private Integer maxActionsResults;
}

/**
* The Front end configuration.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FrontEndConfiguration {

private String url;
@NestedConfigurationProperty
private FrontEndCorsConfiguration cors;

}

/**
* The Front end cors configuration.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FrontEndCorsConfiguration {

private List<String> headers;
private List<String> methods;
private Duration age;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ca.bc.gov.restapi.results.common.security;

import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.stereotype.Component;

@Component
public class ApiAuthorizationCustomizer implements
Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> {

@Override
public void customize(
AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorize) {

authorize
// Allow actuator endpoints to be accessed without authentication
// This is useful for monitoring and health checks
.requestMatchers(HttpMethod.GET, "/actuator/**")
.permitAll()
// Protect everything under /api with authentication
.requestMatchers("/api/**")
.authenticated()
// Allow OPTIONS requests to be accessed with authentication
.requestMatchers(HttpMethod.OPTIONS, "/**")
.authenticated()
// Deny all other requests
.anyRequest().denyAll();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ca.bc.gov.restapi.results.common.security;

import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.stereotype.Component;

@Component
public class CsrfSecurityCustomizer implements Customizer<CsrfConfigurer<HttpSecurity>> {

@Override
public void customize(CsrfConfigurer<HttpSecurity> csrfSpec) {
csrfSpec
.csrfTokenRepository(new CookieCsrfTokenRepository());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ca.bc.gov.restapi.results.common.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;

public class GrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final List<String> realmAccess = new ArrayList<>();
Object clientRolesObj =
jwt
.getClaims()
.getOrDefault("client_roles",List.of());

if (clientRolesObj instanceof List<?> list) {
list.forEach(item -> realmAccess.add(String.valueOf(item)));
}
return realmAccess
.stream()
.map(roleName -> "ROLE_" + roleName)
.map(roleName -> (GrantedAuthority) new SimpleGrantedAuthority(roleName))
.toList();
}
}
Loading

0 comments on commit 9f26ea8

Please sign in to comment.