Merge branch 'dev' into feature/document

This commit is contained in:
2025-07-08 23:13:06 +02:00
3 changed files with 87 additions and 106 deletions

View File

@@ -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);
}
}

View File

@@ -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<Map<String,String>>();
for (var module : config.keys()){
Optional<String> 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");

View File

@@ -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<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException {
if (sessionToken.isEmpty()) return empty();
var session = users.load(sessionToken.get());
return Optional.of(users.load(session));
}
private Optional<UmbrellaUser> 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);
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());
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);
}
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");
}
try {
return update(ex, editedUser,json);
} 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<Token> optToken = SessionToken.from(ex).map(Token::of);
Optional<UmbrellaUser> 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));