Compare commits

...

6 Commits

Author SHA1 Message Date
Pierre Tellier
730aa5f450 Merge remote-tracking branch 'refs/remotes/origin/back-postgres' into back-postgres
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 11s
2025-02-18 16:56:26 +01:00
Pierre Tellier
dda3e5fcfd fix: most likely fixed merge conflict 2025-02-18 16:55:08 +01:00
Pierre Tellier
5e8e875a37 feat: added user deletion and custom api call in the frontend
All checks were successful
CI / build (push) Successful in 11s
2025-02-18 16:45:41 +01:00
Pierre Tellier
86e7dc7c75 fix: removed the test file that was causing the linter to fail
All checks were successful
CI / build (push) Successful in 12s
2025-02-18 16:37:55 +01:00
Pierre Tellier
6235fe7e68 feat: separated class definition
Some checks failed
CI / build (push) Failing after 8s
2025-02-18 12:07:07 +01:00
Pierre Tellier
249d00177c feat: interraction between the backend and keycloak
Some checks failed
CI / build (push) Failing after 9s
2025-02-11 10:00:11 +01:00
9 changed files with 237 additions and 47 deletions

View File

@ -1,14 +1,32 @@
package enseirb.myinpulse.api; package enseirb.myinpulse.api;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import javax.management.relation.RoleNotFoundException;
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication @SpringBootApplication
@RestController @RestController
public class GetUserInfo { public class GetUserInfo {
// TODO: understand how to get data
@GetMapping("/getUserInfo")
public Object user(Principal principal) {
System.out.println("GetUserInfo + " + principal);
System.out.println(SecurityContextHolder.getContext().getAuthentication());
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/unauth/random") @GetMapping("/unauth/random")
public boolean rand() { public boolean rand(@RequestHeader("Authorization") String token) throws RoleNotFoundException {
System.err.println(token);
System.err.println("HELLO");
return Math.random() > 0.5; return Math.random() > 0.5;
} }

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.exceptions;
public class RoleNotFoudException extends RuntimeException {
public RoleNotFoudException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.exceptions;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@ -1,7 +1,9 @@
/*
* Source: https://github.com/ChristianHuff-DEV/secure-spring-rest-api-using-keycloak/blob/main/src/main/java/io/betweendata/RestApi/security/oauth2/KeycloakJwtRolesConverter.java
* edited by Pierre Tellier
*/
package enseirb.myinpulse.security; package enseirb.myinpulse.security;
import static java.util.stream.Collectors.toSet;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@ -16,37 +18,43 @@ import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> { import static java.util.stream.Collectors.toSet;
/** Prefix used for realm level roles. */
public static final String PREFIX_REALM_ROLE = "ROLE_REALM_";
/** Prefix used in combination with the resource (client) name for resource level roles. */
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
/**
* Prefix used for realm level roles.
*/
public static final String PREFIX_REALM_ROLE = "ROLE_REALM_";
/**
* 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(), tokenRolesExtractor(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 * Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes.
* using prefixes.
*/ */
public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) { public Collection<GrantedAuthority> tokenRolesExtractor(Jwt jwt) {
// Collection that will hold the extracted roles // Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>(); Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
@ -61,43 +69,33 @@ 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 = Collection<GrantedAuthority> realmRoles = roles.stream()
roles.stream() // Prefix all realm roles with "ROLE_realm_"
// Prefix all realm roles with "ROLE_realm_" .map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role)) .collect(Collectors.toList());
.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, // A user might have access to multiple resources all containing their own roles. Therefore, it is a map of
// 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 = Map<String, Map<String, Collection<String>>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS);
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( resourceAccess.forEach((resource, resourceClaims) -> {
(resource, resourceClaims) -> { // Iterate of the "roles" claim inside the resource claims
// Iterate of the "roles" claim inside the resource claims resourceClaims.get(CLAIM_ROLES).forEach(
resourceClaims // Add the role to the granted authority prefixed with ROLE_ and the name of the resource
.get(CLAIM_ROLES) role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role))
.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

@ -0,0 +1,135 @@
package enseirb.myinpulse.utils.keycloak;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import enseirb.myinpulse.exceptions.UserNotFoundException;
import enseirb.myinpulse.utils.keycloak.datatypes.RoleRepresentation;
import enseirb.myinpulse.utils.keycloak.datatypes.UserRepresentation;
import org.springframework.web.client.RestClient;
import javax.management.relation.RoleNotFoundException;
public class KeycloakApi {
static final String keycloakUrl;
static final String realmName;
static {
if (System.getenv("VITE_KEYCLOAK_URL") == null) {
System.exit(-1);
}
keycloakUrl = System.getenv("VITE_KEYCLOAK_URL");
}
static {
if (System.getenv("VITE_KEYCLOAK_REALM") == null) {
System.exit(-1);
}
realmName = System.getenv("VITE_KEYCLOAK_REALM");
}
/**
* Uses Keycloak API to retrieve a role representation of a role by its name
*
* @param roleName name of the role
* @param bearer authorization header used by the client to authenticate to keycloak
*/
public static RoleRepresentation getRoleRepresentationByName(String roleName, String bearer)
throws RoleNotFoundException {
RoleRepresentation[] response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.get()
.uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName)
.retrieve()
.body(RoleRepresentation[].class);
if (response == null || response.length == 0) {
throw new RoleNotFoundException("Role not found");
}
return response[0];
}
/**
* Use keycloak API to to retreive a userID via his name or email.
*
* @param username username or mail of the user
* @param bearer bearer of the user, allowing access to database
* @return the userid, as a String
* @throws UserNotFoundException
*/
public static String getUserIdByName(String username, String bearer)
throws UserNotFoundException {
UserRepresentation[] response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.get()
.uri(
"/admin/realms/{realmName}/users?username={username}",
realmName,
username)
.retrieve()
.body(UserRepresentation[].class);
if (response == null || response.length == 0) {
throw new UserNotFoundException("User not found");
}
return response[0].id;
}
/**
* TODO: check for error
*
* <p>Set a keycloak role to a keycloak user.
*
* <p>Usual roles should be `MyINPulse-admin` and `MyINPulse-entrepreneur`
*
* @param username
* @param roleName
* @param bearer
* @throws RoleNotFoundException
* @throws UserNotFoundException
*/
public static void setRoleToUser(String username, String roleName, String bearer)
throws RoleNotFoundException, UserNotFoundException {
RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer);
String userId = getUserIdByName(username, bearer);
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.post()
.uri(
"/admin/realms/${realmName}/users/${userId}/role-mappings/realm",
realmName,
userId)
.body(roleRepresentation)
.contentType(APPLICATION_JSON)
.retrieve();
}
/**
* Delete a user from Keycloak database. TODO: check the bearer permission.
*
* @param username
* @param bearer
* @throws UserNotFoundException
*/
public static void deleteUser(String username, String bearer) throws UserNotFoundException {
String userId = getUserIdByName(username, bearer);
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.delete()
.uri("/admin/realms/${realmName}/users/${userId}", realmName, userId)
.retrieve();
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.utils.keycloak.datatypes;
public class RoleRepresentation {
public String id;
public String name;
public String description;
}

View File

@ -0,0 +1,6 @@
package enseirb.myinpulse.utils.keycloak.datatypes;
public class UserRepresentation {
public String id;
public String name;
}

View File

@ -14,7 +14,8 @@ axiosInstance.interceptors.response.use(
async (error) => { async (error) => {
const originalRequest = error.config; const originalRequest = error.config;
if ( if (
error.response.status === 401 && ((error.response && error.response.status === 401) ||
error.code == "ERR_NETWORK") &&
!originalRequest._retry && !originalRequest._retry &&
store.authenticated store.authenticated
) { ) {

View File

@ -1,6 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { store } from "../main.ts"; import { store } from "../main.ts";
import { callApi } from "@/services/api.ts"; import { callApi } from "@/services/api.ts";
import { ref } from "vue";
const CustomRequest = ref("");
</script> </script>
<template> <template>
@ -55,6 +58,14 @@ import { callApi } from "@/services/api.ts";
<td>res</td> <td>res</td>
<td id="3"></td> <td id="3"></td>
</tr> </tr>
<tr>
<td>
<input v-model="CustomRequest" placeholder="edit me" />
</td>
<td>
<button @click="callApi(CustomRequest)">call</button>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</template> </template>