Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(SILVA-514 + sec): fixing favorite name on search result and security update #491

Merged
merged 23 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
91c5e9a
chore: changing cors
paulushcgcj Nov 20, 2024
9119a2e
chore: changing cors on the frontend
paulushcgcj Nov 20, 2024
b396167
chore: fixing tests on frontend
paulushcgcj Nov 20, 2024
2cb9ddd
Merge branch 'main' into fix/SILVA-514
paulushcgcj Nov 20, 2024
7e9954e
fix(SILVA-514): added favourite flag to search results
paulushcgcj Nov 20, 2024
512c9db
fix(SILVA-514): added favourite/unfavourite to the search list actions
paulushcgcj Nov 20, 2024
f372ec5
Merge branch 'main' into fix/SILVA-514
paulushcgcj Nov 20, 2024
25be865
Merge branch 'main' into fix/SILVA-514
paulushcgcj Nov 21, 2024
e345076
chore: reducing duplicated lines
paulushcgcj Nov 21, 2024
32daa1e
chore: removed unused file
paulushcgcj Nov 21, 2024
d599220
chore: removed unused file
paulushcgcj Nov 21, 2024
b834922
Merge branch 'main' into fix/SILVA-514
paulushcgcj Nov 21, 2024
fbf2adc
chore: removing unused function
paulushcgcj Nov 21, 2024
af1bc68
test(SILVA-514): adding test to function
paulushcgcj Nov 21, 2024
68f47b7
chore: fixing menu and checkboxes
paulushcgcj Nov 21, 2024
6735b43
chore: updated user auth
paulushcgcj Nov 21, 2024
aee64ff
chore: removing unused code and imports
paulushcgcj Nov 21, 2024
dd86ac3
test: fixing some tests
paulushcgcj Nov 21, 2024
2a54666
test: fixing some tests
paulushcgcj Nov 21, 2024
d023c3d
Merge branch 'main' into fix/SILVA-514
paulushcgcj Nov 21, 2024
7c3644a
chore: fixing test value
paulushcgcj Nov 21, 2024
5b0c53b
chor: fixing test
paulushcgcj Nov 21, 2024
a2ee678
chore: ignoring some of the files (for now)
paulushcgcj Nov 21, 2024
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
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
Loading