fix: merged
All checks were successful
Format / formatting (push) Successful in 9s
CI / build (push) Successful in 15s

This commit is contained in:
Théo Le Lez 2025-02-12 10:19:31 +01:00
commit 7a9955b781
17 changed files with 185 additions and 107 deletions

View File

@ -0,0 +1,19 @@
name: Format
on: [ push, pull_request ]
jobs:
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v2 minimum required
- uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- uses: axel-op/googlejavaformat-action@v3
with:
args: "--replace --skip-sorting-imports --aosp"
- name: Print diffs
run: git --no-pager diff --exit-code

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.idea .idea
keycloak/CAS/target keycloak/CAS/target
docker-compose.yaml docker-compose.yaml
postgres/data

View File

@ -27,7 +27,7 @@ dev-front: clean vite
prod: clean prod: clean
@cp config/prod.front.env front/MyINPulse-front/.env @cp config/prod.front.env front/MyINPulse-front/.env
@cp config/prod.main.env .env @cp config/prod.main.env .env
@cp config/frontdev.docker-compose.yaml docker-compose.yaml @cp config/prod.docker-compose.yaml docker-compose.yaml
@docker compose up -d --build @docker compose up -d --build

View File

@ -1,29 +1,15 @@
package enseirb.myinpulse; package enseirb.myinpulse;
import enseirb.myinpulse.security.KeycloakJwtRolesConverter;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.*; import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@SpringBootApplication @SpringBootApplication
public class MyinpulseApplication { public class MyinpulseApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(MyinpulseApplication.class, args); SpringApplication.run(MyinpulseApplication.class, args);
} }
} }

View File

@ -22,21 +22,21 @@ public class GetUserInfo {
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random") @GetMapping("/random")
public boolean rand(){ public boolean rand() {
System.err.println("HELLO"); System.err.println("HELLO");
return Math.random() > 0.5; return Math.random() > 0.5;
} }
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random2") @GetMapping("/random2")
public boolean rand2(){ public boolean rand2() {
System.err.println("HELLO2"); System.err.println("HELLO2");
return Math.random() > 0.5; return Math.random() > 0.5;
} }
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random3") @GetMapping("/random3")
public boolean rand3(){ public boolean rand3() {
System.err.println("HELLO"); System.err.println("HELLO");
return Math.random() > 0.5; return Math.random() > 0.5;
} }

View File

@ -23,10 +23,13 @@ public class WebSecurityCustomConfiguration {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*")); configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS")); configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", configuration.setAllowedHeaders(
"x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated Arrays.asList(
UrlBasedCorsConfigurationSource source = new "authorization",
UrlBasedCorsConfigurationSource(); "content-type",
"x-auth-token")); // Do not remove, this fixes the CORS errors when
// unauthenticated
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); source.registerCorsConfiguration("/**", configuration);
return source; return source;
@ -34,17 +37,23 @@ public class WebSecurityCustomConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http.authorizeHttpRequests(
.authorizeHttpRequests(authorize -> authorize authorize ->
.requestMatchers("/random2").access(hasRole("REALM_MyINPulse-entrepreneur")) authorize
.requestMatchers("/random").access(hasRole("REALM_MyINPulse-admin")) .requestMatchers("/random2")
.requestMatchers("/random3").permitAll() .access(hasRole("REALM_MyINPulse-entrepreneur"))
.anyRequest().authenticated() .requestMatchers("/random")
) .access(hasRole("REALM_MyINPulse-admin"))
.oauth2ResourceServer(oauth2 -> oauth2 .requestMatchers("/random3")
.jwt(jwt -> jwt. .permitAll()
jwtAuthenticationConverter(new KeycloakJwtRolesConverter()))); .anyRequest()
.authenticated())
.oauth2ResourceServer(
oauth2 ->
oauth2.jwt(
jwt ->
jwt.jwtAuthenticationConverter(
new KeycloakJwtRolesConverter())));
return http.build(); return http.build();
} }
} }

View File

@ -16,40 +16,35 @@ import java.util.stream.Stream;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> { public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
/** /** Prefix used for realm level roles. */
* Prefix used for realm level roles.
*/
public static final String PREFIX_REALM_ROLE = "ROLE_REALM_"; public static final String PREFIX_REALM_ROLE = "ROLE_REALM_";
/**
* Prefix used in combination with the resource (client) name for resource level roles. /** Prefix used in combination with the resource (client) name for resource level roles. */
*/
public static final String PREFIX_RESOURCE_ROLE = "ROLE_"; public static final String PREFIX_RESOURCE_ROLE = "ROLE_";
/** /** Name of the claim containing the realm level roles */
* Name of the claim containing the realm level roles
*/
private static final String CLAIM_REALM_ACCESS = "realm_access"; private static final String CLAIM_REALM_ACCESS = "realm_access";
/**
* Name of the claim containing the resources (clients) the user has access to. /** Name of the claim containing the resources (clients) the user has access to. */
*/
private static final String CLAIM_RESOURCE_ACCESS = "resource_access"; private static final String CLAIM_RESOURCE_ACCESS = "resource_access";
/**
* Name of the claim containing roles. (Applicable to realm and resource level.) /** Name of the claim containing roles. (Applicable to realm and resource level.) */
*/
private static final String CLAIM_ROLES = "roles"; private static final String CLAIM_ROLES = "roles";
@Override @Override
public AbstractAuthenticationToken convert(Jwt source) public AbstractAuthenticationToken convert(Jwt source) {
{ return new JwtAuthenticationToken(
return new JwtAuthenticationToken(source, Stream.concat(new JwtGrantedAuthoritiesConverter().convert(source) source,
.stream(), TEMPORARNAME(source).stream()) Stream.concat(
.collect(toSet())); new JwtGrantedAuthoritiesConverter().convert(source).stream(),
TEMPORARNAME(source).stream())
.collect(toSet()));
} }
/** /**
* Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes. * Extracts the realm and resource level roles from a JWT token distinguishing between them
* using prefixes.
*/ */
public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) { public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) {
// Collection that will hold the extracted roles // Collection that will hold the extracted roles
@ -66,33 +61,43 @@ public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthent
// Check if any roles are present // Check if any roles are present
if (roles != null && !roles.isEmpty()) { if (roles != null && !roles.isEmpty()) {
// Iterate of the roles and add them to the granted authorities // Iterate of the roles and add them to the granted authorities
Collection<GrantedAuthority> realmRoles = roles.stream() Collection<GrantedAuthority> realmRoles =
// Prefix all realm roles with "ROLE_realm_" roles.stream()
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role)) // Prefix all realm roles with "ROLE_realm_"
.collect(Collectors.toList()); .map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
.collect(Collectors.toList());
grantedAuthorities.addAll(realmRoles); grantedAuthorities.addAll(realmRoles);
} }
} }
// Resource (client) roles // Resource (client) roles
// A user might have access to multiple resources all containing their own roles. Therefore, it is a map of // A user might have access to multiple resources all containing their own roles. Therefore,
// it is a map of
// resource each possibly containing a "roles" property. // resource each possibly containing a "roles" property.
Map<String, Map<String, Collection<String>>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS); Map<String, Map<String, Collection<String>>> resourceAccess =
jwt.getClaim(CLAIM_RESOURCE_ACCESS);
// Check if resources are assigned // Check if resources are assigned
if (resourceAccess != null && !resourceAccess.isEmpty()) { if (resourceAccess != null && !resourceAccess.isEmpty()) {
// Iterate of all the resources // Iterate of all the resources
resourceAccess.forEach((resource, resourceClaims) -> { resourceAccess.forEach(
// Iterate of the "roles" claim inside the resource claims (resource, resourceClaims) -> {
resourceClaims.get(CLAIM_ROLES).forEach( // Iterate of the "roles" claim inside the resource claims
// Add the role to the granted authority prefixed with ROLE_ and the name of the resource resourceClaims
role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role)) .get(CLAIM_ROLES)
); .forEach(
}); // Add the role to the granted authority prefixed with ROLE_
// and the name of the resource
role ->
grantedAuthorities.add(
new SimpleGrantedAuthority(
PREFIX_RESOURCE_ROLE
+ resource
+ "_"
+ role)));
});
} }
return grantedAuthorities; return grantedAuthorities;
} }
} }

View File

@ -6,8 +6,6 @@ import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest @SpringBootTest
class MyinpulseApplicationTests { class MyinpulseApplicationTests {
@Test @Test
void contextLoads() { void contextLoads() {}
}
} }

View File

@ -1,15 +1,14 @@
services: services:
postgres: postgres:
image: postgres:latest env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
container_name: MyINPulse-DB container_name: MyINPulse-DB
ports: ports:
- 5432:5432 - 5433:5432
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres/data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
keycloak: keycloak:
container_name: MyINPulse-keycloak container_name: MyINPulse-keycloak

View File

@ -1,10 +1,14 @@
POSTGRES_DB=keycloak_db POSTGRES_DB=postgres_db
POSTGRES_USER=keycloak_db_user POSTGRES_USER=postgres
POSTGRES_PASSWORD=keycloak_db_user_password POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
MYINPULSE_DB=MyINPulse_db BACKEND_DB=backend_db
MYINPULSE_DB_USER=MyINPulse_db_user BACKEND_USER=backend_db_user
MYINPULSE_DB_PASS=MyINPulse_db_user_pass BACKEND_PASSWORD=backend_db_user_password

View File

@ -1,15 +1,15 @@
services: services:
postgres: postgres:
image: postgres:latest env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
container_name: MyINPulse-DB container_name: MyINPulse-DB
#ports: #ports:
# - 5432:5432 # - 5432:5432
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres/data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
keycloak: keycloak:
container_name: MyINPulse-keycloak container_name: MyINPulse-keycloak

View File

@ -1,6 +1,14 @@
POSTGRES_DB=keycloak_db POSTGRES_DB=postgres_db
POSTGRES_USER=keycloak_db_user POSTGRES_USER=postgres
POSTGRES_PASSWORD=keycloak_db_user_password POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password

View File

@ -1,11 +1,14 @@
services: services:
postgres: postgres:
image: postgres:latest env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
container_name: MyINPulse-DB container_name: MyINPulse-DB
#ports: #ports:
# - 5432:5432 # - 5432:5432
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres/data:/var/lib/postgresql/data
environment: environment:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}

View File

@ -1,6 +1,14 @@
POSTGRES_DB=keycloak_db POSTGRES_DB=postgres_db
POSTGRES_USER=keycloak_db_user POSTGRES_USER=postgres
POSTGRES_PASSWORD=keycloak_db_user_password POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password

5
postgres/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM postgres:latest
# Custom initialization scripts
COPY ./create_user.sh /docker-entrypoint-initdb.d/10-create_user.sh
COPY ./create_db.sh /docker-entrypoint-initdb.d/20-create_db.sh

17
postgres/create_db.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
set -e
POSTGRES="psql --username ${POSTGRES_USER}"
echo "Creating database: ${DB_NAME}"
$POSTGRES <<EOSQL
CREATE DATABASE ${BACKEND_DB} OWNER ${BACKEND_USER};
EOSQL
echo "Creating database: ${DB_NAME}"
$POSTGRES <<EOSQL
CREATE DATABASE ${KEYCLOAK_DB} OWNER ${KEYCLOAK_USER};
EOSQL

16
postgres/create_user.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
POSTGRES="psql --username ${POSTGRES_USER}"
echo "Creating database role: [${BACKEND_USER}]"
$POSTGRES <<-EOSQL
CREATE USER ${BACKEND_USER} WITH CREATEDB PASSWORD '${BACKEND_PASSWORD}';
EOSQL
echo "Creating database role: ${KEYCLOAK_USER}"
$POSTGRES <<-EOSQL
CREATE USER ${KEYCLOAK_USER} WITH CREATEDB PASSWORD '${KEYCLOAK_PASSWORD}';
EOSQL