From 4ba113daf5e621808a6172cf5c8d93dc6cced700 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 8 Jul 2025 23:12:49 +0200 Subject: [PATCH] unified loading of user in UserModule Signed-off-by: Stephan Richter --- .../srsoftware/umbrella/core/BaseHandler.java | 14 +- .../umbrella/core/model/EmailAddress.java | 5 +- .../srsoftware/umbrella/legacy/LegacyApi.java | 6 +- .../umbrella/message/MessageSystem.java | 1 - .../message/model/CombinedMessage.java | 2 - .../srsoftware/umbrella/user/UserModule.java | 171 +++++++----------- 6 files changed, 88 insertions(+), 111 deletions(-) diff --git a/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java b/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java index 3e18abb..957ac13 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java @@ -4,7 +4,7 @@ package de.srsoftware.umbrella.core; import static de.srsoftware.tools.Optionals.nullable; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.WARNING; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.*; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.tools.Path; @@ -30,6 +30,10 @@ public abstract class BaseHandler extends PathHandler { return ex; } + public boolean forbidden(HttpExchange ex) throws IOException { + return sendEmptyResponse(HTTP_FORBIDDEN,ex); + } + public record Document(String mime, byte[] bytes){} public boolean load(Path path, HttpExchange ex) throws IOException { @@ -58,7 +62,15 @@ public abstract class BaseHandler extends PathHandler { } } + public boolean ok(HttpExchange ex) throws IOException { + return sendEmptyResponse(HTTP_OK,ex); + } + public boolean send(HttpExchange ex, UmbrellaException e) throws IOException { return sendContent(ex,e.statusCode(),e.getMessage()); } + + public boolean unauthorized(HttpExchange ex) throws IOException { + return sendEmptyResponse(HTTP_FORBIDDEN,ex); + } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java index 10451de..665e67f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java @@ -1,11 +1,10 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import de.srsoftware.umbrella.core.UmbrellaException; - import static de.srsoftware.tools.Optionals.allSet; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; -import static java.text.MessageFormat.format; + +import de.srsoftware.umbrella.core.UmbrellaException; public class EmailAddress { private final String email; diff --git a/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java b/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java index ad55a93..c5a14d4 100644 --- a/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java +++ b/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java @@ -92,7 +92,7 @@ public class LegacyApi extends BaseHandler { public boolean getModules(HttpExchange ex) throws IOException { var optToken = SessionToken.from(ex).map(Token::of); - if (optToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + if (optToken.isEmpty()) return unauthorized(ex); var list = new ArrayList>(); for (var module : config.keys()){ Optional url = config.get(module+".baseUrl"); @@ -202,7 +202,7 @@ public class LegacyApi extends BaseHandler { if (!(resp instanceof JSONObject json)) throw new UmbrellaException(500,"{0} did not return JSON!",messageUrl); // TODO: should we return json? - return sendEmptyResponse(HTTP_OK,ex); + return ok(ex); } private boolean logout(HttpExchange ex) throws IOException { @@ -224,7 +224,7 @@ public class LegacyApi extends BaseHandler { private boolean postSearch(HttpExchange ex) throws IOException, UmbrellaException { var optToken = SessionToken.from(ex).map(Token::of); - if (optToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + if (optToken.isEmpty()) return unauthorized(ex); var token = optToken.get(); var json = json(ex); if (!(json.has(KEY) && json.get(KEY) instanceof String key) || key.isBlank()) return sendContent(ex,HTTP_BAD_REQUEST,"No search key given"); diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java b/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java index aef7911..8fdf386 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java +++ b/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java @@ -25,7 +25,6 @@ import jakarta.mail.util.ByteArrayDataSource; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiFunction; -import java.util.function.Function; public class MessageSystem implements PostBox { public static final System.Logger LOG = System.getLogger(MessageSystem.class.getSimpleName()); diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java b/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java index 0fe80a7..ced3efb 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java +++ b/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java @@ -6,10 +6,8 @@ import static java.lang.System.Logger.Level.TRACE; import static java.text.MessageFormat.format; import de.srsoftware.umbrella.core.model.UmbrellaUser; - import java.util.*; import java.util.function.BiFunction; -import java.util.function.Function; public class CombinedMessage { private static final System.Logger LOG = System.getLogger(CombinedMessage.class.getSimpleName()); diff --git a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java index 36083c4..f553e10 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java +++ b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java @@ -21,6 +21,7 @@ import static java.net.HttpURLConnection.*; import static java.nio.charset.StandardCharsets.UTF_8; import static java.text.MessageFormat.format; import static java.time.temporal.ChronoUnit.DAYS; +import static java.util.Optional.empty; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.tools.Path; @@ -94,10 +95,10 @@ public class UserModule extends BaseHandler { } private boolean deleteService(HttpExchange ex, UmbrellaUser user, String serviceName) throws IOException { - if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return unauthorized(ex); try { logins.delete(serviceName); - return sendEmptyResponse(HTTP_OK,ex); + return ok(ex); } catch (UmbrellaException e) { return send(ex,e); } @@ -118,55 +119,55 @@ public class UserModule extends BaseHandler { try { logins.unlink(ForeignLogin.of(serviceId,foreignId,user.id())); - return sendEmptyResponse(HTTP_OK,ex); + return ok(ex); } catch (UmbrellaException e) { return send(ex,e); } + } + + private Optional loadUser(Optional sessionToken) throws UmbrellaException { + if (sessionToken.isEmpty()) return empty(); + var session = users.load(sessionToken.get()); + return Optional.of(users.load(session)); + + } + private Optional loadUser(HttpExchange ex) throws UmbrellaException { + return loadUser(SessionToken.from(ex).map(Token::of)); } @Override public boolean doDelete(Path path, HttpExchange ex) throws IOException { - UmbrellaUser user = null; - var sessionToken = SessionToken.from(ex).map(Token::of); - if (sessionToken.isPresent()) try { - user = users.load(users.load(sessionToken.get())); - } catch (UmbrellaException e) { - LOG.log(WARNING,e); + try { + var user = loadUser(ex); + addCors(ex); + var head = path.pop(); + if (OIDC.equals(head)) return user.isEmpty() ? unauthorized(ex) : deleteOIDC(ex, user.get(), path); + } catch (UmbrellaException e){ + return send(ex,e); } - addCors(ex); - var head = path.pop(); - switch (head) { - case OIDC: return deleteOIDC(ex,user,path); - - }; return super.doDelete(path, ex); } @Override public boolean doGet(Path path, HttpExchange ex) throws IOException { - UmbrellaUser user = null; var sessionToken = SessionToken.from(ex).map(Token::of); - if (sessionToken.isPresent()) try { - user = users.load(users.load(sessionToken.get())); - } catch (UmbrellaException e) { - LOG.log(WARNING,e); - } - addCors(ex); - var head = path.pop(); - switch (head) { - case LIST: return getUserList(ex, user); - case LOGOUT: return logout(ex, sessionToken); - case OIDC: return getOIDC(ex,user,path); - case VALIDATE_OTP: return validateOtpToken(ex,path.pop()); - case WHOAMI: return getUser(ex, user); - - }; try { + var user = loadUser(sessionToken); + addCors(ex); + var head = path.pop(); + switch (head) { + case LIST: return user.isEmpty() ? unauthorized(ex) : getUserList(ex, user.get()); + case LOGOUT: return logout(ex, sessionToken); + case OIDC: return getOIDC(ex,user.orElse(null),path); + case VALIDATE_OTP: return validateOtpToken(ex,path.pop()); + case WHOAMI: return user.isEmpty() ? unauthorized(ex) : sendContent(ex,user.get()); + + }; long userId = Long.parseLong(head); - if (!(user instanceof DbUser dbUser && (user.id() == userId || dbUser.permissions().contains(LIST_USERS)))) { - return sendEmptyResponse(HTTP_FORBIDDEN,ex); - } + if (user.isEmpty()) return forbidden(ex); + if (!(user.get() instanceof DbUser dbUser)) return forbidden(ex); + if (dbUser.id() == userId || dbUser.permissions().contains(LIST_USERS)) return forbidden(ex); return sendContent(ex,users.load(userId)); } catch (UmbrellaException e) { return send(ex,e); @@ -177,56 +178,36 @@ public class UserModule extends BaseHandler { @Override public boolean doOptions(Path path, HttpExchange ex) throws IOException { - return sendEmptyResponse(200,addCors(ex)); + return ok(addCors(ex)); } public boolean doPatch(Path path, HttpExchange ex) throws IOException { addCors(ex); - - var sessionToken = SessionToken.from(ex); - if (sessionToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); - - UmbrellaUser requestingUser; - try { - Session session = users.load(Token.of(sessionToken.get())); - requestingUser = users.load(session); - } catch (UmbrellaException e) { - return send(ex,e); - } - - var head = path.pop(); - long userId; + String head = null; try { + var requestingUser = loadUser(ex); + head = path.pop(); + long userId; if (head == null || head.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"User id missing!"); - if (PASSWORD.equals(head)) return patchPassword(ex,requestingUser); - if (OIDC.equals(head)) return patchService(ex,path.pop(),requestingUser); + if (requestingUser.isEmpty()) return unauthorized(ex); + if (PASSWORD.equals(head)) return patchPassword(ex,requestingUser.get()); + if (OIDC.equals(head)) return patchService(ex,path.pop(),requestingUser.get()); userId = Long.parseLong(head); - } catch (NumberFormatException e) { - return sendContent(ex, HTTP_UNPROCESSABLE,"Invalid user id: "+head); - } - - DbUser editedUser; - try { - editedUser = (DbUser) users.load(userId); - } catch (UmbrellaException e) { - return send(ex,e); - } - - if (requestingUser.id() != userId && (!(requestingUser instanceof DbUser dbUser) || !dbUser.permissions().contains(UPDATE_USERS))){ - return sendContent(ex,HTTP_FORBIDDEN,"You are not allowed to update user "+editedUser.name()); - } - - JSONObject json; - try { - json = json(ex); - } catch (Exception e){ - LOG.log(WARNING,"Request does not contain valid JSON",e); - return sendContent(ex,HTTP_BAD_REQUEST,"Body contains no JSON data"); - } + DbUser editedUser = (DbUser) users.load(userId); + if (!(requestingUser.get() instanceof DbUser dbUser) || !dbUser.permissions().contains(UPDATE_USERS)) return sendContent(ex,HTTP_FORBIDDEN,"You are not allowed to update user "+editedUser.name()); - try { + JSONObject json; + try { + json = json(ex); + } catch (Exception e){ + LOG.log(WARNING,"Request does not contain valid JSON",e); + return sendContent(ex,HTTP_BAD_REQUEST,"Body contains no JSON data"); + } return update(ex, editedUser,json); + + } catch (NumberFormatException e) { + return sendContent(ex, HTTP_UNPROCESSABLE,"Invalid user id: "+head); } catch (UmbrellaException e) { return send(ex,e); } @@ -273,14 +254,8 @@ public class UserModule extends BaseHandler { if (!(resp instanceof JSONObject json)) return sendContent(ex,HTTP_BAD_REQUEST,format("{0} did not return JSON!",location)); if (!json.has(ID_TOKEN)) return sendContent(ex,HTTP_FAILED_DEPENDENCY,"Missing ID token – token exchange failed!"); var idToken = json.getString(ID_TOKEN); - Optional optToken = SessionToken.from(ex).map(Token::of); - Optional optUser = Optional.empty(); - - if (optToken.isPresent()){ - Session session = users.load(optToken.get()); - optUser = Optional.of(users.load(session)); - } var oidcUserId = verifyAndGetUserId(idToken, state); + var optUser = loadUser(ex); if (optUser.isPresent()){ // user already logged in – this is the case when the new id gets assigned var currentUser = optUser.get(); var assignment = new ForeignLogin(state.loginService.name(),oidcUserId,currentUser.id()); @@ -298,7 +273,7 @@ public class UserModule extends BaseHandler { } private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException { - if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + if (user == null) return unauthorized(ex); try { var connections = logins.listAssignments(user.id()).stream().map(ForeignLogin::toMap); return sendContent(ex,connections); @@ -321,7 +296,7 @@ public class UserModule extends BaseHandler { } private boolean getOIDC(HttpExchange ex, UmbrellaUser user, String serviceId) throws IOException { - if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); + if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return forbidden(ex); try { return sendContent(ex,logins.loadLoginService(serviceId).toMap()); } catch (UmbrellaException e) { @@ -381,7 +356,7 @@ public class UserModule extends BaseHandler { } private boolean getServiceList(HttpExchange ex, UmbrellaUser user) throws IOException { - if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); + if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return forbidden(ex); try { var services = logins.listLoginServices().stream().map(LoginService::toMap); return sendContent(ex,services); @@ -392,11 +367,6 @@ public class UserModule extends BaseHandler { } } - private boolean getUser(HttpExchange ex, UmbrellaUser user) throws IOException { - if (user != null) return sendContent(ex,user); - return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); - } - private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException { if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS))) return sendContent(ex,HTTP_FORBIDDEN,"You are not allowed to list users!"); try { @@ -408,11 +378,10 @@ public class UserModule extends BaseHandler { } private boolean impersonate(HttpExchange ex, Long targetId) throws IOException { - var sessionToken = SessionToken.from(ex).map(Token::of); - if (sessionToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); try { - var requestingUser = users.load(users.load(sessionToken.get())); - if (!(requestingUser instanceof DbUser dbUser && dbUser.permissions().contains(PERMISSION.IMPERSONATE))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); + var requestingUser = loadUser(ex); + if (!(requestingUser.isPresent() && requestingUser.get() instanceof DbUser dbUser)) return unauthorized(ex); + if (!dbUser.permissions().contains(PERMISSION.IMPERSONATE)) return forbidden(ex); if (targetId == null) return sendContent(ex,HTTP_UNPROCESSABLE,"user id missing"); var targetUser = users.load(targetId); users.getSession(targetUser).cookie().addTo(ex); @@ -431,9 +400,9 @@ public class UserModule extends BaseHandler { } new SessionToken(token.toString(),"/", Instant.now().minus(1, DAYS),true).addTo(ex); - return sendEmptyResponse(HTTP_OK,ex); + return ok(ex); } - return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + return unauthorized(ex); } @@ -468,11 +437,11 @@ public class UserModule extends BaseHandler { } private boolean postCreate(HttpExchange ex) throws IOException { - var sessionToken = SessionToken.from(ex).map(Token::of); - if (sessionToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); + try { - var u = users.load(users.load(sessionToken.get())); - if (!(u instanceof DbUser requestingUser && requestingUser.permissions().contains(PERMISSION.CREATE_USERS))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); + var optUser = loadUser(ex); + if (!(optUser.isPresent() && optUser.get() instanceof DbUser dbUser)) return unauthorized(ex); + if (!dbUser.permissions().contains(PERMISSION.CREATE_USERS)) return forbidden(ex); var json = json(ex); if (json.has(USER)) json = json.getJSONObject(USER); @@ -511,11 +480,11 @@ public class UserModule extends BaseHandler { } catch (UmbrellaException e){ return send(ex,e); } - return sendEmptyResponse(HTTP_OK,ex); + return ok(ex); } private boolean patchService(HttpExchange ex, String serviceName, UmbrellaUser requestingUser) throws IOException { - if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); + if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) return forbidden(ex); try { var json = json(ex); if (!json.has(NAME) || !(json.get(NAME) instanceof String name) || name.isBlank()) return sendContent(ex,HTTP_UNPROCESSABLE,format(ERROR_MISSING_FIELD,NAME));