wip: implement ws base

This commit is contained in:
2025-11-20 23:39:02 +01:00
parent 22fa896ee0
commit a83088387b
17 changed files with 657 additions and 9 deletions

View File

@@ -59,6 +59,10 @@ public class CompetitionModel {
String owner;
List<String> admin = new ArrayList<>();
@Column(name = "table_")
List<String> table = new ArrayList<>();
String data1;
String data2;
String data3;

View File

@@ -219,13 +219,58 @@ public class CompetPermService {
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaEditPerm(securityCtx, o.getId());
if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here
throw new DForbiddenException();
if (o.getSystem() == CompetitionSystem.NONE)
if (securityCtx.isClubAdmin())
if (o.getSystem() == CompetitionSystem.NONE) {
if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
throw new DForbiddenException();
}
throw new DForbiddenException();
})
);
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, CompetitionModel competitionModel) {
return hasTablePerm(securityCtx, Uni.createFrom().item(competitionModel));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, long id) {
return hasTablePerm(securityCtx, competitionRepository.findById(id));
}
/**
* @return {@link fr.titionfire.ffsaf.data.model.CompetitionModel} if securityCtx has edit perm
*/
public Uni<CompetitionModel> hasTablePerm(SecurityCtx securityCtx, Uni<CompetitionModel> in) {
return in.call(Unchecked.function(o -> {
if (securityCtx.getSubject().equals(o.getOwner()) || securityCtx.roleHas("federation_admin"))
return Uni.createFrom().nullItem();
if (o.getSystem() == CompetitionSystem.SAFCA)
return hasSafcaTablePerm(securityCtx, o.getId());
if (o.getSystem() == CompetitionSystem.NONE) {
if (securityCtx.isInClubGroup(o.getClub().getId()) && securityCtx.isClubAdmin())
return Uni.createFrom().nullItem();
if (o.getAdmin().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
if (o.getTable().contains(securityCtx.getSubject()))
return Uni.createFrom().nullItem();
throw new DForbiddenException();
}
throw new DForbiddenException();
})
);
@@ -236,8 +281,8 @@ public class CompetPermService {
Uni.createFrom().nullItem()
:
getSafcaConfig(id).chain(Unchecked.function(o -> {
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject())) && !o.table()
.contains(UUID.fromString(securityCtx.getSubject())))
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject()))
&& !o.table().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}));
@@ -253,4 +298,16 @@ public class CompetPermService {
return Uni.createFrom().nullItem();
}));
}
private Uni<?> hasSafcaTablePerm(SecurityCtx securityCtx, long id) {
return securityCtx.roleHas("safca_super_admin") ?
Uni.createFrom().nullItem()
:
getSafcaConfig(id).chain(Unchecked.function(o -> {
if (!o.admin().contains(UUID.fromString(securityCtx.getSubject()))
&& !o.table().contains(UUID.fromString(securityCtx.getSubject())))
throw new DForbiddenException();
return Uni.createFrom().nullItem();
}));
}
}

View File

@@ -56,7 +56,7 @@ public class ResultService {
public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(m -> categoryRepository.list("compet.uuid = ?1", uuid)
.chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1) OR category IN ?2", //TODO AND
.chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True
m.getMembre(), cats)))
.map(matchModels -> {
HashMap<Long, List<MatchModel>> map = new HashMap<>();

View File

@@ -0,0 +1,176 @@
package fr.titionfire.ffsaf.ws;
import com.fasterxml.jackson.core.JsonProcessingException;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.domain.service.CompetPermService;
import fr.titionfire.ffsaf.net2.MessageType;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
import fr.titionfire.ffsaf.ws.recv.RMatch;
import fr.titionfire.ffsaf.ws.recv.WSReceiver;
import fr.titionfire.ffsaf.ws.send.JsonUni;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.security.Authenticated;
import io.quarkus.websockets.next.*;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.ForbiddenException;
import org.jboss.logging.Logger;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@Authenticated
@WebSocket(path = "api/ws/competition/{uuid}")
public class CompetitionWS {
private static final Logger LOGGER = Logger.getLogger(CompetitionWS.class);
private static final HashMap<WebSocketConnection, HashMap<UUID, JsonUni<?>>> waitingResponse = new HashMap<>();
@Inject
RMatch rMatch;
@Inject
SecurityCtx securityCtx;
@Inject
CompetPermService competPermService;
@Inject
CompetitionRepository competitionRepository;
HashMap<Method, Object> wsMethods = new HashMap<>();
public void getWSReceiverMethods(Class<?> clazz, Object object) {
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(WSReceiver.class)) {
if (method.getParameterCount() <= 1) {
LOGGER.warnf("@WSReceiver has no parameters for method %s", method.getName());
continue;
}
if (!method.getReturnType().equals(Uni.class)) {
LOGGER.warnf("@WSReceiver has returned unexpected type %s", method.getReturnType());
continue;
}
// method.setAccessible(true);
wsMethods.put(method, object);
}
}
}
@PostConstruct
void init() {
getWSReceiverMethods(RMatch.class, rMatch);
}
@OnOpen
@WithSession
Uni<MessageOut> open(WebSocketConnection connection) {
LOGGER.infof("Opening CompetitionWS for %s", connection.pathParam("uuid"));
LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size());
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
.invoke(Unchecked.consumer(cm -> {
if (cm == null)
throw new ForbiddenException();
}))
.call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> "admin")
.onFailure()
.recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> "table"))
.onFailure()
.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> "view"))
.invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem))
.invoke(prem -> LOGGER.infof("Connection permission: %s", prem))
.onFailure().transform(t -> new ForbiddenException()))
.invoke(__ -> {
connection.userData().put(UserData.TypedKey.forString("subject"), securityCtx.getSubject());
waitingResponse.put(connection, new HashMap<>());
})
.map(cm -> {
WelcomeInfo welcomeInfo = new WelcomeInfo();
welcomeInfo.setName(cm.getName());
welcomeInfo.setPerm(connection.userData().get(UserData.TypedKey.forString("prem")));
return new MessageOut(UUID.randomUUID(), "welcomeInfo", MessageType.NOTIFY, welcomeInfo);
});
}
@OnClose
void close(WebSocketConnection connection) {
LOGGER.infof("Closing CompetitionWS for %s ", connection.pathParam("uuid"));
LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size());
waitingResponse.remove(connection);
}
private MessageOut makeReply(MessageIn message, Object data) {
return new MessageOut(message.uuid(), message.code(), MessageType.REPLY, data);
}
private MessageOut makeError(MessageIn message, Object data) {
return new MessageOut(message.uuid(), message.code(), MessageType.ERROR, data);
}
@OnTextMessage
Multi<MessageOut> processAsync(WebSocketConnection connection, MessageIn message) {
if (message.type() == MessageType.REPLY || message.type() == MessageType.ERROR) {
try {
JsonUni<?> jsonUni = waitingResponse.get(connection).get(message.uuid());
if (jsonUni == null) {
LOGGER.debugf("No JsonUni found for %s", message.uuid());
if (message.type() == MessageType.ERROR)
LOGGER.errorf("request %s make error %s", message.uuid(), message.data());
return null;
}
waitingResponse.get(connection).remove(message.uuid());
if (message.type() == MessageType.ERROR)
return jsonUni.castAndError(message.data()).onFailure()
.invoke(t -> LOGGER.error(t.getMessage(), t))
.replaceWith((MessageOut) null).toMulti().filter(__ -> false);
return jsonUni.castAndChain(message.data()).onFailure()
.invoke(t -> LOGGER.error(t.getMessage(), t))
.replaceWith((MessageOut) null).toMulti().filter(__ -> false);
} catch (JsonProcessingException e) {
LOGGER.warn(e.getMessage(), e);
}
}
try {
for (Map.Entry<Method, Object> entry : wsMethods.entrySet()) {
Method method = entry.getKey();
if (method.getAnnotation(WSReceiver.class).code().equalsIgnoreCase(message.code())) {
return ((Uni<?>) method.invoke(entry.getValue(), connection,
MAPPER.treeToValue(message.data(), method.getParameterTypes()[1])))
.map(o -> makeReply(message, o))
.onFailure()
.recoverWithItem(t -> makeError(message, t.getMessage())).toMulti()
.filter(__ -> message.type() == MessageType.REQUEST);
}
}
return Uni.createFrom().item(makeError(message, "No receiver method found")).toMulti();
} catch (IllegalAccessException | InvocationTargetException | JsonProcessingException e) {
LOGGER.warn(e.getMessage(), e);
return Uni.createFrom().item(makeError(message, e.getMessage())).toMulti();
}
// return Uni.createFrom().item(new Message<>(message.uuid(), message.code(), MessageType.REPLY, "ko"));
}
@OnError
Uni<Void> error(WebSocketConnection connection, ForbiddenException t) {
return connection.close(CloseReason.INTERNAL_SERVER_ERROR);
//return "forbidden: " + securityCtx.getSubject();
}
}

View File

@@ -0,0 +1,12 @@
package fr.titionfire.ffsaf.ws;
import com.fasterxml.jackson.databind.JsonNode;
import fr.titionfire.ffsaf.net2.MessageType;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.UUID;
@RegisterForReflection
public record MessageIn (UUID uuid, String code, MessageType type, JsonNode data){
}

View File

@@ -0,0 +1,11 @@
package fr.titionfire.ffsaf.ws;
import fr.titionfire.ffsaf.net2.MessageType;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.UUID;
@RegisterForReflection
public record MessageOut(UUID uuid, String code, MessageType type, Object data){
}

View File

@@ -0,0 +1,11 @@
package fr.titionfire.ffsaf.ws.data;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
@Data
@RegisterForReflection
public class WelcomeInfo {
private String name;
private String perm;
}

View File

@@ -0,0 +1,29 @@
package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
@WithSession
@ApplicationScoped
@RegisterForReflection
public class RMatch {
private static final Logger LOGGER = Logger.getLogger(RMatch.class);
@Inject
MatchRepository matchRepository;
@WSReceiver(code = "getAllMatch")
public Uni<?> getAllMatch(WebSocketConnection connection, Long l) {
LOGGER.info("getAllMatch " + l);
return Uni.createFrom().item(l);
//return matchRepository.count();
}
}

View File

@@ -0,0 +1,14 @@
package fr.titionfire.ffsaf.ws.recv;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@RegisterForReflection
@Retention(RetentionPolicy.RUNTIME)
public @interface WSReceiver {
String code();
}

View File

@@ -0,0 +1,33 @@
package fr.titionfire.ffsaf.ws.send;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import java.util.function.Function;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@RegisterForReflection
public class JsonUni<T> {
private final Class<T> clazz;
private final Function<? super T, Uni<?>> mapper;
public JsonUni(Class<T> clazz, Function<? super T, Uni<?>> mapper) {
this.clazz = clazz;
this.mapper = mapper;
}
public Uni<?> castAndChain(JsonNode message) throws JsonProcessingException {
return Uni.createFrom().item(MAPPER.treeToValue(message, clazz)).chain(mapper);
}
public Uni<?> castAndError(JsonNode message) throws JsonProcessingException {
return Uni.createFrom().item((T) null).invoke(Unchecked.consumer(__ -> {
throw new WSClientError(MAPPER.treeToValue(message, String.class));
})).chain(mapper);
}
}

View File

@@ -0,0 +1,13 @@
package fr.titionfire.ffsaf.ws.send;
import java.io.IOException;
import java.io.Serial;
public class WSClientError extends IOException {
@Serial
private static final long serialVersionUID = -3790479241838684450L;
public WSClientError(String message) {
super(message);
}
}