|
|
|
|
@ -6,11 +6,12 @@ import static de.srsoftware.umbrella.core.Constants.*;
@@ -6,11 +6,12 @@ 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_SERVER_ERROR; |
|
|
|
|
import static de.srsoftware.umbrella.user.Constants.*; |
|
|
|
|
import static de.srsoftware.umbrella.user.Paths.LOGIN; |
|
|
|
|
import static de.srsoftware.umbrella.user.Paths.WHOAMI; |
|
|
|
|
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.LIST_USERS; |
|
|
|
|
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.UPDATE_USERS; |
|
|
|
|
import static de.srsoftware.umbrella.user.Paths.*; |
|
|
|
|
import static de.srsoftware.umbrella.user.Paths.IMPERSONATE; |
|
|
|
|
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.time.temporal.ChronoUnit.DAYS; |
|
|
|
|
@ -19,8 +20,8 @@ import com.sun.net.httpserver.HttpExchange;
@@ -19,8 +20,8 @@ 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.IOException; |
|
|
|
|
@ -35,6 +36,7 @@ public class UserModule extends PathHandler {
@@ -35,6 +36,7 @@ public class UserModule extends PathHandler {
|
|
|
|
|
private static final BadHasher BAD_HASHER; |
|
|
|
|
private static final System.Logger LOG = System.getLogger("User"); |
|
|
|
|
private final UserDb users; |
|
|
|
|
private final LoginServiceDb logins; |
|
|
|
|
|
|
|
|
|
static { |
|
|
|
|
try { |
|
|
|
|
@ -44,8 +46,9 @@ public class UserModule extends PathHandler {
@@ -44,8 +46,9 @@ public class UserModule extends PathHandler {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public UserModule(UserDb userDb){ |
|
|
|
|
public UserModule(UserDb userDb, LoginServiceDb loginDb){ |
|
|
|
|
users = userDb; |
|
|
|
|
logins = loginDb; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private HttpExchange addCors(HttpExchange ex){ |
|
|
|
|
@ -77,6 +80,7 @@ public class UserModule extends PathHandler {
@@ -77,6 +80,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 WHOAMI: return getUser(ex, user); |
|
|
|
|
|
|
|
|
|
}; |
|
|
|
|
@ -149,49 +153,52 @@ public class UserModule extends PathHandler {
@@ -149,49 +153,52 @@ public class UserModule extends PathHandler {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean getUserList(HttpExchange ex, UmbrellaUser user) throws IOException { |
|
|
|
|
if (user instanceof DbUser dbUser && dbUser.permissions().contains(LIST_USERS)){ |
|
|
|
|
try { |
|
|
|
|
var list = users.list(0, null).stream().map(UmbrellaUser::toMap).toList(); |
|
|
|
|
return sendContent(ex,list); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} |
|
|
|
|
@Override |
|
|
|
|
public boolean doPost(Path path, HttpExchange ex) throws IOException { |
|
|
|
|
addCors(ex); |
|
|
|
|
var head = path.pop(); |
|
|
|
|
Long targetId = null; |
|
|
|
|
try { |
|
|
|
|
targetId = Long.parseLong(head); |
|
|
|
|
head = path.pop(); |
|
|
|
|
} catch (NumberFormatException ignored) {} |
|
|
|
|
switch (head){ |
|
|
|
|
case IMPERSONATE: return impersonate(ex,targetId); |
|
|
|
|
case LOGIN: return postLogin(ex); |
|
|
|
|
} |
|
|
|
|
return sendContent(ex,HTTP_FORBIDDEN,"You are not allowed to list users!"); |
|
|
|
|
return super.doPost(path, ex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean patchPassword(HttpExchange ex, UmbrellaUser requestingUser) throws IOException { |
|
|
|
|
if (!(requestingUser instanceof DbUser user)) return sendContent(ex, ResponseCode.HTTP_SERVER_ERROR,"DbUser expected"); |
|
|
|
|
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"); |
|
|
|
|
} |
|
|
|
|
if (!json.has("old") || !(json.get("old") instanceof String oldpass) || oldpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"old password missing!"); |
|
|
|
|
if (!json.has("new") || !(json.get("new") instanceof String newpass) || newpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"new password missing!"); |
|
|
|
|
var old = Password.of(BAD_HASHER.hash(oldpass,null)); |
|
|
|
|
if (!user.hashedPassword().equals(old)) return sendContent(ex,HTTP_UNAUTHORIZED,"Wrong password (old)"); |
|
|
|
|
if (weak(newpass)) return sendContent(ex,HTTP_BAD_REQUEST,"New password too weak!"); |
|
|
|
|
var pass = Password.of(BAD_HASHER.hash(newpass,null)); |
|
|
|
|
private boolean getService(HttpExchange ex, UmbrellaUser user, Path path) throws IOException { |
|
|
|
|
var head = path.pop(); |
|
|
|
|
return switch (head){ |
|
|
|
|
case BUTTONS -> getOidcButtons(ex); |
|
|
|
|
case LIST -> getServiceList(ex,user); |
|
|
|
|
case null, default -> super.doGet(path,ex); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean getOidcButtons(HttpExchange ex) throws IOException { |
|
|
|
|
try { |
|
|
|
|
var updated = users.save(new DbUser(user.id(), user.name(), user.email(), pass, user.theme(), user.language(), user.permissions(), null)); |
|
|
|
|
return sendContent(ex, updated); |
|
|
|
|
var services = logins.listLoginServices().stream().map(LoginService::name); |
|
|
|
|
return sendContent(ex,services); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} catch (IOException e) { |
|
|
|
|
return sendContent(ex,HTTP_SERVER_ERROR,e.getMessage()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public boolean doPost(Path path, HttpExchange ex) throws IOException { |
|
|
|
|
addCors(ex); |
|
|
|
|
var p = path.toString(); |
|
|
|
|
switch (p){ |
|
|
|
|
case LOGIN: return postLogin(ex); |
|
|
|
|
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); |
|
|
|
|
try { |
|
|
|
|
var services = logins.listLoginServices().stream().map(LoginService::toMap); |
|
|
|
|
return sendContent(ex,services); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} catch (IOException e) { |
|
|
|
|
return sendContent(ex,HTTP_SERVER_ERROR,e.getMessage()); |
|
|
|
|
} |
|
|
|
|
return super.doPost(path, ex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean getUser(HttpExchange ex, UmbrellaUser user) throws IOException { |
|
|
|
|
@ -199,6 +206,33 @@ public class UserModule extends PathHandler {
@@ -199,6 +206,33 @@ public class UserModule extends PathHandler {
|
|
|
|
|
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 { |
|
|
|
|
var list = users.list(0, null).stream().map(UmbrellaUser::toMap).toList(); |
|
|
|
|
return sendContent(ex,list); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
if (targetId == null) return sendContent(ex,HTTP_UNPROCESSABLE,"user id missing"); |
|
|
|
|
var targetUser = users.load(targetId); |
|
|
|
|
users.getSession(targetUser) |
|
|
|
|
.cookie() |
|
|
|
|
.addTo(ex.getResponseHeaders()); |
|
|
|
|
return sendContent(ex,targetUser.toMap()); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public boolean logout(HttpExchange ex, Optional<Token> optToken) throws IOException { |
|
|
|
|
if (optToken.isPresent()){ |
|
|
|
|
var token = optToken.get(); |
|
|
|
|
@ -213,6 +247,29 @@ public class UserModule extends PathHandler {
@@ -213,6 +247,29 @@ public class UserModule extends PathHandler {
|
|
|
|
|
return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean patchPassword(HttpExchange ex, UmbrellaUser requestingUser) throws IOException { |
|
|
|
|
if (!(requestingUser instanceof DbUser user)) return sendContent(ex, HTTP_SERVER_ERROR,"DbUser expected"); |
|
|
|
|
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"); |
|
|
|
|
} |
|
|
|
|
if (!json.has("old") || !(json.get("old") instanceof String oldpass) || oldpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"old password missing!"); |
|
|
|
|
if (!json.has("new") || !(json.get("new") instanceof String newpass) || newpass.isBlank()) return sendContent(ex, HTTP_UNPROCESSABLE,"new password missing!"); |
|
|
|
|
var old = Password.of(BAD_HASHER.hash(oldpass,null)); |
|
|
|
|
if (!user.hashedPassword().equals(old)) return sendContent(ex,HTTP_UNAUTHORIZED,"Wrong password (old)"); |
|
|
|
|
if (weak(newpass)) return sendContent(ex,HTTP_BAD_REQUEST,"New password too weak!"); |
|
|
|
|
var pass = Password.of(BAD_HASHER.hash(newpass,null)); |
|
|
|
|
try { |
|
|
|
|
var updated = users.save(new DbUser(user.id(), user.name(), user.email(), pass, user.theme(), user.language(), user.permissions(), null)); |
|
|
|
|
return sendContent(ex, updated); |
|
|
|
|
} catch (UmbrellaException e) { |
|
|
|
|
return sendContent(ex,e.statusCode(),e.getMessage()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean postLogin(HttpExchange ex) throws IOException { |
|
|
|
|
var json = json(ex); |
|
|
|
|
if (!(json.has(USERNAME) && json.get(USERNAME) instanceof String username)) return sendContent(ex, HTTP_UNPROCESSABLE,"Username missing"); |
|
|
|
|
@ -230,6 +287,16 @@ public class UserModule extends PathHandler {
@@ -230,6 +287,16 @@ public class UserModule extends PathHandler {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static int score(String password){ |
|
|
|
|
if (password == null) return 0; |
|
|
|
|
var score = 0; |
|
|
|
|
for (int i=0; i<password.length(); i++){ |
|
|
|
|
int c = password.charAt(i); |
|
|
|
|
score += Character.isDigit(c) || Character.isLetter(c) ? 1 : 3; |
|
|
|
|
} |
|
|
|
|
return score; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException { |
|
|
|
|
var id = user.id(); |
|
|
|
|
var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name(); |
|
|
|
|
@ -241,16 +308,6 @@ public class UserModule extends PathHandler {
@@ -241,16 +308,6 @@ public class UserModule extends PathHandler {
|
|
|
|
|
return sendContent(ex,HTTP_OK,saved); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static int score(String password){ |
|
|
|
|
if (password == null) return 0; |
|
|
|
|
var score = 0; |
|
|
|
|
for (int i=0; i<password.length(); i++){ |
|
|
|
|
int c = password.charAt(i); |
|
|
|
|
score += Character.isDigit(c) || Character.isLetter(c) ? 1 : 3; |
|
|
|
|
} |
|
|
|
|
return score; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static boolean weak(String password){ |
|
|
|
|
return score(password) < 14; |
|
|
|
|
}; |
|
|
|
|
|