173 lines
8.6 KiB
Java
173 lines
8.6 KiB
Java
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 fr.titionfire.ffsaf.utils.RequiredAction;
|
|
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.RoleScopeResource;
|
|
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()).getSubGroups(0, 1000, true).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<List<String>> fetchRole(String id) {
|
|
return vertx.getOrCreateContext().executeBlocking(() ->
|
|
keycloak.realm(realm).users().get(id).roles().realmLevel().listEffective().stream().map(RoleRepresentation::getName).toList());
|
|
}
|
|
|
|
public Uni<?> updateRole(String id, List<String> toAdd, List<String> toRemove) {
|
|
return vertx.getOrCreateContext().executeBlocking(() -> {
|
|
RoleScopeResource resource = keycloak.realm(realm).users().get(id).roles().realmLevel();
|
|
List<RoleRepresentation> roles = keycloak.realm(realm) .roles().list();
|
|
resource.add(roles.stream().filter(r -> toAdd.contains(r.getName())).toList());
|
|
resource.remove(roles.stream().filter(r -> toRemove.contains(r.getName())).toList());
|
|
return "OK";
|
|
});
|
|
}
|
|
|
|
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.setEmail(membreModel.getEmail());
|
|
user.setEnabled(true);
|
|
|
|
user.setRequiredActions(List.of(RequiredAction.VERIFY_EMAIL.name(),
|
|
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)));
|
|
})
|
|
.invoke(user -> membreModel.setUserId(user.getId()))
|
|
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
|
|
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
|
|
}
|
|
|
|
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) {
|
|
}
|
|
}
|