feat: add keycloak account creation and club groupe set
This commit is contained in:
@@ -21,6 +21,8 @@ public class ClubModel {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
String clubId;
|
||||
|
||||
String name;
|
||||
|
||||
String country;
|
||||
|
||||
@@ -25,6 +25,8 @@ public class MembreModel {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
String userId;
|
||||
|
||||
String lname;
|
||||
String fname;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.Map;
|
||||
public class ClubEntity {
|
||||
private long id;
|
||||
private String name;
|
||||
private String clubId;
|
||||
private String country;
|
||||
private String shieldURL;
|
||||
private Map<Contact, String> contact;
|
||||
@@ -35,6 +36,7 @@ public class ClubEntity {
|
||||
return ClubEntity.builder()
|
||||
.id(model.getId())
|
||||
.name(model.getName())
|
||||
.clubId(model.getClubId())
|
||||
.country(model.getCountry())
|
||||
.shieldURL(model.getShieldURL())
|
||||
.contact(model.getContact())
|
||||
@@ -49,7 +51,7 @@ public class ClubEntity {
|
||||
}
|
||||
|
||||
public ClubModel toModel () {
|
||||
return new ClubModel(this.id, this.name, this.country, this.shieldURL, this.contact, this.training_location,
|
||||
return new ClubModel(this.id, this.clubId, this.name, this.country, this.shieldURL, this.contact, this.training_location,
|
||||
this.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation,
|
||||
this.international);
|
||||
}
|
||||
|
||||
@@ -32,4 +32,11 @@ public class ClubService {
|
||||
public Uni<List<ClubModel>> getAll() {
|
||||
return repository.listAll();
|
||||
}
|
||||
|
||||
public Uni<?> setClubId(Long id, String id1) {
|
||||
return repository.findById(id).chain(clubModel -> {
|
||||
clubModel.setClubId(id1);
|
||||
return Panache.withTransaction(() -> repository.persist(clubModel));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.utils.KeycloakException;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import io.vertx.mutiny.core.Vertx;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@ApplicationScoped
|
||||
public class KeycloakService {
|
||||
private static final Logger LOGGER = Logger.getLogger(KeycloakService.class);
|
||||
|
||||
@Inject
|
||||
Keycloak keycloak;
|
||||
|
||||
@Inject
|
||||
ClubService clubService;
|
||||
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
@ConfigProperty(name = "keycloak.realm")
|
||||
String realm;
|
||||
|
||||
@Inject
|
||||
Vertx vertx;
|
||||
|
||||
public Uni<String> getGroupFromClub(ClubModel club) {
|
||||
if (club.getClubId() == null) {
|
||||
LOGGER.infof("Creation of club group %d-%s...", club.getId(), club.getName());
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
GroupRepresentation clubGroup = keycloak.realm(realm).groups().groups().stream().filter(g -> g.getName().equals("club"))
|
||||
.findAny().orElseThrow(() -> new KeycloakException("Fail to fetch group %s".formatted("club")));
|
||||
|
||||
GroupRepresentation groupRepresentation = new GroupRepresentation();
|
||||
groupRepresentation.setName(club.getId() + "-" + club.getName());
|
||||
|
||||
try (Response response = keycloak.realm(realm).groups().group(clubGroup.getId()).subGroup(groupRepresentation)) {
|
||||
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
||||
throw new KeycloakException("Fail to set group parent for club: %s (reason=%s)".formatted(club.getName(),
|
||||
response.getStatusInfo().getReasonPhrase()));
|
||||
}
|
||||
|
||||
return keycloak.realm(realm).groups().group(clubGroup.getId()).toRepresentation().getSubGroups().stream()
|
||||
.filter(g -> g.getName().startsWith(club.getId() + "-")).findAny().map(GroupRepresentation::getId)
|
||||
.orElseThrow(() -> new KeycloakException("Fail to fetch group %s*".formatted(club.getId() + "-")));
|
||||
}
|
||||
).call(id -> clubService.setClubId(club.getId(), id));
|
||||
}
|
||||
return Uni.createFrom().item(club::getClubId);
|
||||
}
|
||||
|
||||
public Uni<String> getUserFromMember(MembreModel membreModel) {
|
||||
if (membreModel.getUserId() == null) {
|
||||
return Uni.createFrom().failure(new NullPointerException("No keycloak user linked to the user id=" + membreModel.getId()));
|
||||
}
|
||||
return Uni.createFrom().item(membreModel::getUserId);
|
||||
}
|
||||
|
||||
public Uni<String> setClubGroupMembre(MembreModel membreModel, ClubModel club) {
|
||||
return getGroupFromClub(club).chain(
|
||||
clubId -> getUserFromMember(membreModel).chain(userId -> vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
UserResource user = keycloak.realm(realm).users().get(userId);
|
||||
user.groups().stream().filter(g -> g.getPath().startsWith("/club")).forEach(g -> user.leaveGroup(g.getId()));
|
||||
user.joinGroup(clubId);
|
||||
LOGGER.infof("Set club \"%s\" to user %s (%s)", club.getName(), userId, user.toRepresentation().getUsername());
|
||||
return "OK";
|
||||
})));
|
||||
}
|
||||
|
||||
public Uni<UserCompteState> fetchCompte(String id) {
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
UserResource user = keycloak.realm(realm).users().get(id);
|
||||
UserRepresentation user2 = user.toRepresentation();
|
||||
return new UserCompteState(user2.isEnabled(), user2.getUsername(), user2.isEmailVerified(),
|
||||
user.roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList(),
|
||||
user.groups().stream().map(GroupRepresentation::getName).toList());
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<String> initCompte(long id) {
|
||||
return membreService.getById(id).invoke(Unchecked.consumer(membreModel -> {
|
||||
if (membreModel.getUserId() != null)
|
||||
throw new KeycloakException("User already linked to the user id=" + id);
|
||||
if (membreModel.getEmail() == null)
|
||||
throw new KeycloakException("User email is null");
|
||||
if (membreModel.getFname() == null || membreModel.getLname() == null)
|
||||
throw new KeycloakException("User name is null");
|
||||
})).chain(membreModel -> creatUser(membreModel).chain(user -> {
|
||||
LOGGER.infof("Set user id %s to membre %s", user.getId(), membreModel.getId());
|
||||
return membreService.setUserId(membreModel.getId(), user.getId());
|
||||
}))
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
|
||||
private Uni<UserRepresentation> creatUser(MembreModel membreModel) {
|
||||
String login = makeLogin(membreModel);
|
||||
LOGGER.infof("Creation of user %s...", login);
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(login);
|
||||
user.setFirstName(membreModel.getFname());
|
||||
user.setLastName(membreModel.getLname());
|
||||
user.setEnabled(true);
|
||||
|
||||
//user.setRequiredActions(List.of(UserModel.RequiredAction.VERIFY_EMAIL.name(),
|
||||
// UserModel.RequiredAction.UPDATE_PASSWORD.name()));
|
||||
|
||||
try (Response response = keycloak.realm(realm).users().create(user)) {
|
||||
if (!response.getStatusInfo().equals(Response.Status.CREATED) && !response.getStatusInfo().equals(Response.Status.CONFLICT))
|
||||
throw new KeycloakException("Fail to creat user %s (reason=%s)".formatted(login, response.getStatusInfo().getReasonPhrase()));
|
||||
}
|
||||
|
||||
return getUser(login).orElseThrow(() -> new KeycloakException("Fail to fetch user %s".formatted(login)));
|
||||
}).call(user -> membreService.setUserId(membreModel.getId(), user.getId()));
|
||||
}
|
||||
|
||||
private Optional<UserRepresentation> getUser(String username) {
|
||||
List<UserRepresentation> users = keycloak.realm(realm).users().searchByUsername(username, true);
|
||||
|
||||
if (users.isEmpty())
|
||||
return Optional.empty();
|
||||
else
|
||||
return Optional.of(users.get(0));
|
||||
}
|
||||
|
||||
private String makeLogin(MembreModel model) {
|
||||
return Normalizer.normalize((model.getFname().toLowerCase() + "." + model.getLname().toLowerCase()).replace(' ', '_'), Normalizer.Form.NFD)
|
||||
.replaceAll("\\p{M}", "");
|
||||
|
||||
}
|
||||
|
||||
public record UserCompteState(Boolean enabled, String login, Boolean emailVerified, List<String> realmRoles,
|
||||
List<String> groups) {
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public class MembreService {
|
||||
ClubRepository clubRepository;
|
||||
@Inject
|
||||
ServerCustom serverCustom;
|
||||
@Inject
|
||||
KeycloakService keycloakService;
|
||||
|
||||
public SimpleCombModel find(int licence, String np) throws Throwable {
|
||||
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
|
||||
@@ -65,6 +67,15 @@ public class MembreService {
|
||||
return Panache.withTransaction(() -> repository.persist(m));
|
||||
})
|
||||
.invoke(membreModel -> SReqComb.sendIfNeed(serverCustom.clients, SimpleCombModel.fromModel(membreModel)))
|
||||
.call(membreModel -> (membreModel.getUserId() != null) ?
|
||||
keycloakService.setClubGroupMembre(membreModel, membreModel.getClub()) : Uni.createFrom().nullItem())
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
|
||||
public Uni<?> setUserId(Long id, String id1) {
|
||||
return repository.findById(id).chain(membreModel -> {
|
||||
membreModel.setUserId(id1);
|
||||
return Panache.withTransaction(() -> repository.persist(membreModel));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.rest.data.UserInfo;
|
||||
import io.quarkus.security.Authenticated;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -9,6 +10,7 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -22,6 +24,9 @@ public class AuthEndpoints {
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@Inject
|
||||
JsonWebToken accessToken;
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public Boolean auth() {
|
||||
@@ -30,9 +35,10 @@ public class AuthEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("/userinfo")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public String userinfo() {
|
||||
return securityIdentity.getPrincipal().getName();
|
||||
@Authenticated
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public UserInfo userinfo() {
|
||||
return UserInfo.makeUserInfo(accessToken, securityIdentity);
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
31
src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java
Normal file
31
src/main/java/fr/titionfire/ffsaf/rest/CompteEndpoints.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.KeycloakService;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
|
||||
@Path("api/compte")
|
||||
public class CompteEndpoints {
|
||||
|
||||
@Inject
|
||||
KeycloakService service;
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@RolesAllowed("federation_admin")
|
||||
public Uni<?> getCompte(@PathParam("id") String id) {
|
||||
return service.fetchCompte(id);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{id}/init")
|
||||
@RolesAllowed("federation_admin")
|
||||
public Uni<?> initCompte(@PathParam("id") long id) {
|
||||
return service.initCompte(id);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import java.util.Date;
|
||||
@RegisterForReflection
|
||||
public class SimpleMembre {
|
||||
private long id;
|
||||
private String userId;
|
||||
private String lname = "";
|
||||
private String fname = "";
|
||||
private Categorie categorie;
|
||||
@@ -38,6 +39,7 @@ public class SimpleMembre {
|
||||
|
||||
return new SimpleMembreBuilder()
|
||||
.id(model.getId())
|
||||
.userId(model.getUserId())
|
||||
.lname(model.getLname())
|
||||
.fname(model.getFname())
|
||||
.categorie(model.getCategorie())
|
||||
|
||||
38
src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java
Normal file
38
src/main/java/fr/titionfire/ffsaf/rest/data/UserInfo.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
public class UserInfo {
|
||||
String id;
|
||||
String name;
|
||||
String givenName;
|
||||
String familyName;
|
||||
String email;
|
||||
boolean emailVerified;
|
||||
long expiration;
|
||||
Set<String> groups;
|
||||
Set<String> roles;
|
||||
|
||||
public static UserInfo makeUserInfo(JsonWebToken accessToken, SecurityIdentity securityIdentity) {
|
||||
UserInfo.UserInfoBuilder builder = UserInfo.builder();
|
||||
builder.id(accessToken.getSubject());
|
||||
builder.name(accessToken.getName());
|
||||
builder.givenName(accessToken.getClaim("given_name"));
|
||||
builder.familyName(accessToken.getClaim("family_name"));
|
||||
builder.email(accessToken.getClaim("email"));
|
||||
builder.emailVerified(accessToken.getClaim("email_verified"));
|
||||
builder.expiration(accessToken.getExpirationTime());
|
||||
builder.groups(accessToken.getGroups());
|
||||
builder.roles(securityIdentity.getRoles());
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
public class KeycloakException extends Exception{
|
||||
|
||||
public KeycloakException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user