diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java index b9122d5..9347ac3 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java @@ -4,6 +4,7 @@ package de.srsoftware.oidc.api; import de.srsoftware.oidc.api.data.User; import java.util.List; import java.util.Optional; +import java.util.Set; public interface UserService { /** @@ -22,6 +23,7 @@ public interface UserService { public Optional forToken(String accessToken); public UserService init(User defaultUser); public List list(); + public Set find(String key); public Optional load(String id); public Optional load(String username, String password); public boolean passwordMatches(String password, String hashedPassword); diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java index aa3a017..c15cbd6 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java @@ -1,4 +1,4 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api.data; -public enum Permission { MANAGE_CLIENTS } +public enum Permission { MANAGE_CLIENTS, MANAGE_USERS } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java index bbec403..4ac42c3 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java @@ -2,6 +2,7 @@ package de.srsoftware.oidc.api.data; import java.util.*; +import org.json.JSONObject; public final class User { public static final String EMAIL = "email"; @@ -23,8 +24,8 @@ public final class User { this.uuid = uuid; } - public User add(Permission permission) { - permissions.add(permission); + public User add(Permission... newPermissions) { + for (var permission : newPermissions) permissions.add(permission); return this; } @@ -68,6 +69,21 @@ public final class User { return includePassword ? Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, PASSWORD, hashedPassword) : Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid); } + public static Optional of(JSONObject json, String userId) { + var user = new User(json.getString(USERNAME), json.getString(PASSWORD), json.getString(REALNAME), json.getString(EMAIL), userId); + + var perms = json.has(PERMISSIONS) ? json.getJSONArray(PERMISSIONS) : Set.of(); + for (Object perm : perms) { + try { + if (perm instanceof String s) perm = Permission.valueOf(s); + if (perm instanceof Permission p) user.add(p); + } catch (Exception e) { + e.printStackTrace(); + } + } + return Optional.of(user); + } + public String realName() { return realName; } diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index 4ecd1e3..20611ac 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -4,6 +4,7 @@ package de.srsoftware.oidc.app; import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.data.Permission.MANAGE_CLIENTS; +import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS; import static de.srsoftware.utils.Optionals.emptyIfBlank; import static de.srsoftware.utils.Paths.configDir; import static de.srsoftware.utils.Strings.uuid; @@ -52,7 +53,7 @@ public class Application { var keyDir = storageFile.getParentFile().toPath().resolve("keys"); var passwordHasher = new UuidHasher(); var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); - var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS); + var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS, MANAGE_USERS); KeyStorage keyStore = new PlaintextKeyStore(keyDir); KeyManager keyManager = new RotatingKeyManager(keyStore); FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser); diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java index d4ad046..88c82c9 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java @@ -1,7 +1,10 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.backend; +import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS; import static de.srsoftware.oidc.api.data.User.*; +import static de.srsoftware.utils.Strings.uuid; +import static java.lang.System.Logger.Level.WARNING; import static java.net.HttpURLConnection.*; import com.sun.net.httpserver.HttpExchange; @@ -22,6 +25,15 @@ public class UserController extends Controller { users = userService; } + private boolean addUser(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + if (!user.hasPermission(MANAGE_USERS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var json = json(ex); + var newID = uuid(); + User.of(json, uuid()).ifPresent(u -> users.updatePassword(u, json.getString(PASSWORD))); + return sendContent(ex, newID); + } + @Override public boolean doGet(String path, HttpExchange ex) throws IOException { switch (path) { @@ -41,20 +53,14 @@ public class UserController extends Controller { return notFound(ex); } - private boolean userInfo(HttpExchange ex) throws IOException { - var optUser = getBearer(ex).flatMap(users::forToken); - if (optUser.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - var user = optUser.get(); - var map = Map.of("sub", user.uuid(), "email", user.email()); - return sendContent(ex, new JSONObject(map)); - } - @Override public boolean doPost(String path, HttpExchange ex) throws IOException { switch (path) { case "/login": return login(ex); + case "/reset": + return resetPassword(ex); } var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); @@ -64,6 +70,10 @@ public class UserController extends Controller { switch (path) { case "/": return sendUserAndCookie(ex, session); + case "/add": + return addUser(ex, session); + case "/list": + return list(ex, session); case "/password": return updatePassword(ex, session); case "/update": @@ -72,6 +82,14 @@ public class UserController extends Controller { return notFound(ex); } + private boolean list(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + if (!user.hasPermission(MANAGE_USERS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var json = new JSONObject(); + users.list().forEach(u -> json.put(u.uuid(), u.map(false))); + return sendContent(ex, json); + } + private boolean login(HttpExchange ex) throws IOException { var body = json(ex); @@ -89,6 +107,16 @@ public class UserController extends Controller { return sendEmptyResponse(HTTP_OK, ex); } + private boolean resetPassword(HttpExchange ex) throws IOException { + var idOrEmail = body(ex); + users.find(idOrEmail).forEach(this::senPasswordLink); + return sendEmptyResponse(HTTP_OK, ex); + } + + private void senPasswordLink(User user) { + LOG.log(WARNING, "Sending password link to {0}", user.email()); + } + private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException { new SessionToken(session.id()).addTo(ex); return sendContent(ex, session.user().map(false)); @@ -122,7 +150,16 @@ public class UserController extends Controller { } user.username(json.getString(USERNAME)); user.email(json.getString(EMAIL)); + user.realName(json.getString(REALNAME)); users.save(user); return sendContent(ex, user.map(false)); } + + private boolean userInfo(HttpExchange ex) throws IOException { + var optUser = getBearer(ex).flatMap(users::forToken); + if (optUser.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + var user = optUser.get(); + var map = Map.of("sub", user.uuid(), "email", user.email()); + return sendContent(ex, new JSONObject(map)); + } } diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index f4e5368..5c3c3fb 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -31,6 +31,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe private static final String SESSIONS = "sessions"; private static final String USERS = "users"; private static final String USER = "user"; + private static final List KEYS = List.of(USERNAME, EMAIL, REALNAME); private final Path storageFile; private final JSONObject json; @@ -91,10 +92,24 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe return this; } + @Override + public Set find(String key) { + var users = json.getJSONObject(USERS); + var result = new HashSet(); + for (var id : users.keySet()) { + var data = users.getJSONObject(id); + if (id.equals(key)) User.of(data, id).ifPresent(result::add); + if (KEYS.stream().map(data::getString).anyMatch(val -> val.equals(key))) User.of(data, id).ifPresent(result::add); + } + return result; + } @Override public List list() { - return List.of(); + var users = json.getJSONObject(USERS); + List result = new ArrayList<>(); + for (var uid : users.keySet()) User.of(users.getJSONObject(uid), uid).ifPresent(result::add); + return result; } @@ -103,24 +118,23 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe try { var users = json.getJSONObject(USERS); var userData = users.getJSONObject(userId); - return userOf(userData, userId); + return User.of(userData, userId); } catch (Exception ignored) { } return empty(); } @Override - public Optional load(String username, String password) { + public Optional load(String user, String password) { try { var users = json.getJSONObject(USERS); var uuids = users.keySet(); for (String userId : uuids) { var userData = users.getJSONObject(userId); - if (!userData.getString(USERNAME).equals(username)) continue; + + if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(user))) continue; var hashedPass = userData.getString(PASSWORD); - if (passwordHasher.matches(password, hashedPass)) { - return userOf(userData, userId); - } + if (passwordHasher.matches(password, hashedPass)) return User.of(userData, userId); } return empty(); } catch (Exception e) { @@ -141,34 +155,16 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } else { users = json.getJSONObject(USERS); } + users.put(user.uuid(), user.map(true)); return save(); } @Override public FileStore updatePassword(User user, String plaintextPassword) { - var oldHashedPassword = user.hashedPassword(); - var salt = passwordHasher.salt(oldHashedPassword); - user.hashedPassword(passwordHasher.hash(plaintextPassword, salt)); - return save(user); + return save(user.hashedPassword(passwordHasher.hash(plaintextPassword, uuid()))); } - private Optional userOf(JSONObject json, String userId) { - var user = new User(json.getString(USERNAME), json.getString(PASSWORD), json.getString(REALNAME), json.getString(EMAIL), userId); - var perms = json.getJSONArray(PERMISSIONS); - for (Object perm : perms) { - try { - if (perm instanceof String s) perm = Permission.valueOf(s); - if (perm instanceof Permission p) user.add(p); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return Optional.of(user); - } - - /*** Session Service Methods ***/ // TODO: prolong session on user activity diff --git a/de.srsoftware.oidc.web/src/main/resources/de/clients.html b/de.srsoftware.oidc.web/src/main/resources/DE/clients.html similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/de/clients.html rename to de.srsoftware.oidc.web/src/main/resources/DE/clients.html diff --git a/de.srsoftware.oidc.web/src/main/resources/de/edit_client.html b/de.srsoftware.oidc.web/src/main/resources/DE/edit_client.html similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/de/edit_client.html rename to de.srsoftware.oidc.web/src/main/resources/DE/edit_client.html diff --git a/de.srsoftware.oidc.web/src/main/resources/de/login.html b/de.srsoftware.oidc.web/src/main/resources/DE/login.html similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/de/login.html rename to de.srsoftware.oidc.web/src/main/resources/DE/login.html diff --git a/de.srsoftware.oidc.web/src/main/resources/de/logout.html b/de.srsoftware.oidc.web/src/main/resources/DE/logout.html similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/de/logout.html rename to de.srsoftware.oidc.web/src/main/resources/DE/logout.html diff --git a/de.srsoftware.oidc.web/src/main/resources/de/navigation.html b/de.srsoftware.oidc.web/src/main/resources/DE/navigation.html similarity index 88% rename from de.srsoftware.oidc.web/src/main/resources/de/navigation.html rename to de.srsoftware.oidc.web/src/main/resources/DE/navigation.html index 7ba2a95..a9263c4 100644 --- a/de.srsoftware.oidc.web/src/main/resources/de/navigation.html +++ b/de.srsoftware.oidc.web/src/main/resources/DE/navigation.html @@ -2,4 +2,5 @@ Clients Benutzer Einstellungen +TODO Abmelden diff --git a/de.srsoftware.oidc.web/src/main/resources/de/new_client.html b/de.srsoftware.oidc.web/src/main/resources/DE/new_client.html similarity index 100% rename from de.srsoftware.oidc.web/src/main/resources/de/new_client.html rename to de.srsoftware.oidc.web/src/main/resources/DE/new_client.html diff --git a/de.srsoftware.oidc.web/src/main/resources/de/settings.html b/de.srsoftware.oidc.web/src/main/resources/DE/settings.html similarity index 89% rename from de.srsoftware.oidc.web/src/main/resources/de/settings.html rename to de.srsoftware.oidc.web/src/main/resources/DE/settings.html index 266b09a..1fceb4e 100644 --- a/de.srsoftware.oidc.web/src/main/resources/de/settings.html +++ b/de.srsoftware.oidc.web/src/main/resources/DE/settings.html @@ -21,6 +21,10 @@ Benutzername + + Anzeigename + + E-Mail @@ -48,6 +52,10 @@ altes Passwort + + + + Neues Passwort diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.html b/de.srsoftware.oidc.web/src/main/resources/en/login.html index 3edecb0..a3a4583 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.html @@ -14,7 +14,10 @@ - + @@ -28,8 +31,15 @@ + + + +
User name + + +
Password
+