|
|
|
|
@@ -6,7 +6,6 @@ import static de.srsoftware.umbrella.core.Constants.*;
|
|
|
|
|
import static de.srsoftware.umbrella.core.Paths.LIST;
|
|
|
|
|
import static de.srsoftware.umbrella.core.Paths.LOGOUT;
|
|
|
|
|
import static de.srsoftware.umbrella.core.ResponseCode.*;
|
|
|
|
|
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_NOT_IMPLEMENTED;
|
|
|
|
|
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
|
|
|
|
|
import static de.srsoftware.umbrella.user.Constants.*;
|
|
|
|
|
import static de.srsoftware.umbrella.user.Paths.*;
|
|
|
|
|
@@ -15,6 +14,7 @@ import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION;
|
|
|
|
|
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.*;
|
|
|
|
|
import static java.lang.System.Logger.Level.WARNING;
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
@@ -22,24 +22,34 @@ import com.sun.net.httpserver.HttpExchange;
|
|
|
|
|
import de.srsoftware.tools.Path;
|
|
|
|
|
import de.srsoftware.tools.PathHandler;
|
|
|
|
|
import de.srsoftware.tools.SessionToken;
|
|
|
|
|
import de.srsoftware.umbrella.core.ResponseCode;
|
|
|
|
|
import de.srsoftware.umbrella.core.UmbrellaException;
|
|
|
|
|
import de.srsoftware.umbrella.user.api.LoginServiceDb;
|
|
|
|
|
import de.srsoftware.umbrella.user.api.UserDb;
|
|
|
|
|
import de.srsoftware.umbrella.user.model.*;
|
|
|
|
|
|
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.net.*;
|
|
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Optional;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
|
|
import org.json.JSONObject;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class UserModule extends PathHandler {
|
|
|
|
|
|
|
|
|
|
private record State(LoginService loginService, JSONObject config){
|
|
|
|
|
public static State of(LoginService loginService, JSONObject config) {
|
|
|
|
|
return new State(loginService,config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final BadHasher BAD_HASHER;
|
|
|
|
|
private static final System.Logger LOG = System.getLogger("User");
|
|
|
|
|
private final UserDb users;
|
|
|
|
|
private final LoginServiceDb logins;
|
|
|
|
|
private final HashMap<String, State> stateMep = new HashMap<>(); // map from state to OIDC provider name
|
|
|
|
|
|
|
|
|
|
static {
|
|
|
|
|
try {
|
|
|
|
|
@@ -83,7 +93,7 @@ public class UserModule extends PathHandler {
|
|
|
|
|
switch (head) {
|
|
|
|
|
case LIST: return getUserList(ex, user);
|
|
|
|
|
case LOGOUT: return logout(ex, sessionToken);
|
|
|
|
|
case SERVICE: return getService(ex,user,path);
|
|
|
|
|
case OIDC: return getService(ex,user,path);
|
|
|
|
|
case WHOAMI: return getUser(ex, user);
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
@@ -124,7 +134,7 @@ public class UserModule extends PathHandler {
|
|
|
|
|
try {
|
|
|
|
|
if (head == null || head.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"User id missing!");
|
|
|
|
|
if (PASSWORD.equals(head)) return patchPassword(ex,requestingUser);
|
|
|
|
|
if (SERVICE.equals(head)) return patchService(ex,path.pop(),requestingUser);
|
|
|
|
|
if (OIDC.equals(head)) return patchService(ex,path.pop(),requestingUser);
|
|
|
|
|
userId = Long.parseLong(head);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return sendContent(ex, HTTP_UNPROCESSABLE,"Invalid user id: "+head);
|
|
|
|
|
@@ -178,11 +188,74 @@ public class UserModule extends PathHandler {
|
|
|
|
|
return switch (head){
|
|
|
|
|
case BUTTONS -> getOidcButtons(ex);
|
|
|
|
|
case LIST -> getServiceList(ex,user);
|
|
|
|
|
case CONNECTED -> getConnectedServices(ex,user);
|
|
|
|
|
case REDIRECT -> getOidcRedirect(ex,path.pop());
|
|
|
|
|
case null -> super.doGet(path,ex);
|
|
|
|
|
default -> getService(ex,user,head);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static HttpURLConnection open(URL url) throws IOException {
|
|
|
|
|
var conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
conn.setRequestProperty("Accept","*/*");
|
|
|
|
|
conn.setRequestProperty("Host",url.getHost());
|
|
|
|
|
conn.setRequestProperty("User-Agent","Umbrella/0.1");
|
|
|
|
|
return conn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private JSONObject getOidcConfig(LoginService service) throws UmbrellaException {
|
|
|
|
|
URL url = null;
|
|
|
|
|
try {
|
|
|
|
|
url = new URI(service.url()+"/.well-known/openid-configuration").toURL();
|
|
|
|
|
var is = open(url).getInputStream();
|
|
|
|
|
var bos = new ByteArrayOutputStream();
|
|
|
|
|
is.transferTo(bos);
|
|
|
|
|
is.close();
|
|
|
|
|
return new JSONObject(bos.toString(UTF_8));
|
|
|
|
|
} catch (MalformedURLException | URISyntaxException e) {
|
|
|
|
|
throw new UmbrellaException(500,"Failed to create URL from \"{0}\"!",service.url()).causedBy(e);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new UmbrellaException(500,"Failed to read from \"{0}\"!",url).causedBy(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean getOidcRedirect(HttpExchange ex, String name) throws IOException {
|
|
|
|
|
try {
|
|
|
|
|
var loginService = logins.loadLoginService(name);
|
|
|
|
|
var config = getOidcConfig(loginService);
|
|
|
|
|
if (!config.has(AUTH_ENDPOINT)) {
|
|
|
|
|
LOG.log(WARNING, "OpenID-Configuration does not declare an {0}!", AUTH_ENDPOINT);
|
|
|
|
|
throw new UmbrellaException(424, "No authorization endpoint configured for {0}", loginService.name());
|
|
|
|
|
}
|
|
|
|
|
var returnTo = queryParam(ex).get("returnTo");
|
|
|
|
|
if (isSet(returnTo)) config.put("returnTo",returnTo);
|
|
|
|
|
var url = url(ex);
|
|
|
|
|
var callback = url.replaceAll("/api/.*", "/callback"); // TODO: frontendPath an zweiter stelle
|
|
|
|
|
var authEndpoint = config.getString(AUTH_ENDPOINT);
|
|
|
|
|
var clientId = loginService.clientId();
|
|
|
|
|
var state = UUID.randomUUID().toString();
|
|
|
|
|
stateMep.put(state, State.of(loginService, config));
|
|
|
|
|
return sendContent(ex,Map.of(OIDC_CALLBACK, callback, AUTH_ENDPOINT, authEndpoint, SCOPE, OIDC_SCOPE, CLIENT_ID, clientId, RESPONSE_TYPE, CODE, STATE, state));
|
|
|
|
|
} catch (UmbrellaException e){
|
|
|
|
|
return send(ex,e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean send(HttpExchange ex, UmbrellaException e) throws IOException {
|
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException {
|
|
|
|
|
if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
|
|
|
|
try {
|
|
|
|
|
var connections = logins.listAssignments(user.id()).stream().map(ForeignLogin::toMap);
|
|
|
|
|
return sendContent(ex,connections);
|
|
|
|
|
} catch (UmbrellaException e) {
|
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean getService(HttpExchange ex, UmbrellaUser user, String serviceId) throws IOException {
|
|
|
|
|
if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex);
|
|
|
|
|
try {
|
|
|
|
|
|