From 323c71402b48a5c8315a691b8e7c466395fabd7d Mon Sep 17 00:00:00 2001
From: Flokster This method searches for a key with the specified value and returns the corresponding
+ * {@link Key} object if found. If no matching key is found, it returns {@code null}. This method performs the following actions:
+ *
+
@@ -31,6 +54,9 @@
+ *
+ *
The {@code keys} table stores information about authentication keys, including their + * unique identifier, name, value, and associated role. This method ensures that the + * table structure is in place before any key-related operations are performed.
+ * + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ + /*public static synchronized void createAllowedModelsTable() throws SQLException { + String sql = "CREATE TABLE IF NOT EXISTS allowed_models (\n" + + " key_value TEXT NOT NULL,\n" + + " model_name TEXT NOT NULL,\n" + + " PRIMARY KEY (key_value, model_name)\n" + + ");"; + + try (Connection conn = connect(); Statement statement = conn.createStatement()) { + statement.execute(sql); + Logger.info("Allowed models table created or already exists."); + } + } + + */ + + /** * Inserts a new {@link Key} into the {@code keys} table. * @@ -206,12 +232,79 @@ public static synchronized boolean deleteKeyByValue(String value) throws SQLExce if (affectedRows > 0) { Logger.info("Key deleted: " + value); return true; - } else { - Logger.warn("No key found to delete with name: " + value); - return false; + } + + } + return false; + } + public static synchronized boolean changeKeyNameByName(String oldName, String newName) throws SQLException { + String sql = "UPDATE keys SET name = ? WHERE name = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, newName); + stmt.setString(2, oldName); + + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key name updated: " + oldName + " → " + newName); + return true; } } + return false; } + public static synchronized boolean changeKeyRoleByName(String name, Role newRole) throws SQLException { + String sql = "UPDATE keys SET role = ? WHERE name = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, newRole.toString()); + stmt.setString(2, name); + + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key role updated: " + name + " → " + newRole); + return true; + } + } + return false; + } + + public static synchronized boolean changeKeyRoleByAuth(String val, Role newRole) throws SQLException { + String sql = "UPDATE keys SET role = ? WHERE value = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, newRole.name()); + stmt.setString(2, val); + + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key role updated: " + val + " → " + newRole); + return true; + } + } + return false; + } + + + public static synchronized boolean changeKeyNameByAuth(String value, String newName) throws SQLException { + String sql = "UPDATE keys SET value = ? WHERE name = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, newName); + stmt.setString(2, value); + + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key name updated: " + value + " → " + newName); + return true; + } + } + return false; + } + } diff --git a/src/main/java/upr/famnit/managers/connections/Management.java b/src/main/java/upr/famnit/managers/connections/Management.java index 86f82ae..00ada48 100644 --- a/src/main/java/upr/famnit/managers/connections/Management.java +++ b/src/main/java/upr/famnit/managers/connections/Management.java @@ -8,6 +8,7 @@ import upr.famnit.components.*; import upr.famnit.managers.DatabaseManager; import upr.famnit.managers.Overseer; +import upr.famnit.util.LogLevel; import upr.famnit.util.Logger; import upr.famnit.util.StreamUtil; @@ -118,7 +119,6 @@ public void run() { } - /** * Handles requests to the "/queue" route, managing operations related to the request queue. * @@ -144,7 +144,8 @@ private void handleKeyRoute() throws IOException, SQLException { switch (clientRequest.getRequest().getMethod()) { case "GET" -> handleListKeysRequest(); case "POST" -> handleInsertKeyRequest(); - case "DELETE"->handleDeleteKeyRequest(); + case "DELETE" -> handleDeleteKeyRequest(); + case "UPDATE" -> handleKeyChangeReq(); case null, default -> respond(ResponseFactory.NotFound()); } } @@ -209,7 +210,7 @@ private void handleWorkerHiveVersionRoute() throws IOException { } } - private void handleWorkerCommandRoute() throws IOException { + private void handleWorkerCommandRoute() throws IOException { switch (clientRequest.getRequest().getMethod()) { case "POST" -> handleWorkersCommandRequest(); case null, default -> respond(ResponseFactory.NotFound()); @@ -355,7 +356,7 @@ private void handleDeleteKeyRequest() throws IOException, SQLException { String authValue = jsonObject.get("auth").getAsString(); Key requester = DatabaseManager.getKeyByValue(authValue); - if (requester == null || (requester.getRole())!=Role.Admin) { + if (requester == null || (requester.getRole()) != Role.Admin) { respond(ResponseFactory.MethodNotAllowed()); return; } @@ -367,40 +368,39 @@ private void handleDeleteKeyRequest() throws IOException, SQLException { try { - if(!value.isEmpty()){ - key=DatabaseManager.getKeyByValue(value); - deleteByValue=true; + if (!value.isEmpty()) { + key = DatabaseManager.getKeyByValue(value); + deleteByValue = true; } - if(key==null&&!name.isEmpty()) { + if (key == null && !name.isEmpty()) { key = DatabaseManager.getKeyByName(name); } if (key != null) { - if(deleteByValue){ - boolean deleted=DatabaseManager.deleteKeyByValue(key.getValue()); - } - else if (!deleteByValue&&key.getRole()!=Role.Admin) { + if (deleteByValue) { + boolean deleted = DatabaseManager.deleteKeyByValue(key.getValue()); + Logger.log("Key deleted by value: " + key.getValue(), LogLevel.success); + respond(ResponseFactory.Ok(value.getBytes())); + return; + } else if (!deleteByValue && key.getRole() != Role.Admin) { // Key exists and is NOT Admin → delete it Logger.log(key.toString()); boolean deleted = DatabaseManager.deleteKeyByName(key.getName()); if (deleted) { - System.out.println("Key deleted: " + key.getName()); - } else { - System.out.println("Failed to delete key: " + key.getName()); + Logger.log("Key deleted: " + key.getName(), LogLevel.success); + respond(ResponseFactory.Ok(name.getBytes())); + return; + } - } else { - // Admin key → do not delete - System.out.println("Cannot delete Admin key: " + key.getName()); } - } else { - // Key does not exist - System.out.println("Key not found: " + name); } + respond(ResponseFactory.NotFound()); + Logger.log("Key could not be deleted", LogLevel.error); } catch (SQLException e) { e.printStackTrace(); + //respond(ResponseFactory.BadRequest()); } - respond(ResponseFactory.Ok(name.getBytes())); } @@ -437,6 +437,82 @@ private void handleListKeysRequest() throws IOException { respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); } + private void handleKeyChangeReq() throws IOException, SQLException { + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); + + JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject(); + + String authValue = jsonObject.get("auth").getAsString(); + Key requester = DatabaseManager.getKeyByValue(authValue); + boolean update = false; + + if (requester == null || (requester.getRole()) != Role.Admin) { + respond(ResponseFactory.MethodNotAllowed()); + return; + } + String auth = jsonObject.has("value") ? jsonObject.get("value").getAsString().trim() : ""; + String name = jsonObject.has("name") ? jsonObject.get("name").getAsString().trim() : ""; + String newRole = jsonObject.has("roleNew") ? jsonObject.get("roleNew").getAsString().trim() : ""; + String newName = jsonObject.has("newName") ? jsonObject.get("newName").getAsString().trim() : ""; + boolean isnewName = !newName.isEmpty(); + boolean hasNewRole = !newRole.isEmpty(); + boolean bothChange = isnewName && hasNewRole; + try { + + + if (!auth.isEmpty() && name.isEmpty()) { + + if (bothChange) { + update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + update = DatabaseManager.changeKeyNameByAuth(auth, newName); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + Logger.log("Change of name and role succesfull", LogLevel.success); + return; + } else if (isnewName) { + update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + return; + + } else if (hasNewRole) { + update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + return; + } + respond(ResponseFactory.BadRequest()); + Logger.log("Cant update", LogLevel.error); + + } else if (!name.isEmpty() && auth.isEmpty()) { + if (bothChange) { + update = DatabaseManager.changeKeyRoleByName(name, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + update = DatabaseManager.changeKeyNameByName(name, newName); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + Logger.log("Change of name and role succesfull", LogLevel.success); + return; + } else if (isnewName) { + update = DatabaseManager.changeKeyRoleByName(name, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + return; + + } else if (hasNewRole) { + update = DatabaseManager.changeKeyRoleByName(name, Role.fromString(newRole)); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + return; + } + respond(ResponseFactory.BadRequest()); + Logger.log("Cant update", LogLevel.error); + return; + + } + }catch (SQLException e){ + e.printStackTrace(); + } + if (!update){ + respond(ResponseFactory.BadRequest()); + } + } + /** * Determines whether the incoming request is from an authenticated administrator. * From 167ab834f7a80c9e8dbbcb3e0fd99272a1dcc03d Mon Sep 17 00:00:00 2001 From: FloksterThis method attempts to remove a key whose {@code name} matches the specified value. + * It is commonly used for administrative cleanup operations or key management tasks.
+ * + * @param name the name of the key to delete + * @return {@code true} if a key with the given name was successfully deleted, + * {@code false} if no matching key was found + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ public static synchronized boolean deleteKeyByName(String name) throws SQLException { String sql = "DELETE FROM keys WHERE name = ?"; @@ -224,6 +235,18 @@ public static synchronized boolean deleteKeyByName(String name) throws SQLExcept } } } + + /** + * Deletes a key from the {@code keys} table based on its value. + * + *This method removes a key whose {@code value} field matches the specified authentication + * token. It is typically used when invalidating or rotating API keys or credentials.
+ * + * @param value the key value (token) to delete + * @return {@code true} if a key with the given value was deleted, {@code false} otherwise + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ + public static synchronized boolean deleteKeyByValue(String value) throws SQLException{ String sql = "DELETE FROM keys WHERE value = ?"; try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -237,6 +260,18 @@ public static synchronized boolean deleteKeyByValue(String value) throws SQLExce } return false; } + /** + * Updates the name of a key in the {@code keys} table based on its current name. + * + *This method locates a key using its existing {@code name} and updates it to the + * provided {@code newName}. It is useful for renaming keys in administrative settings.
+ * + * @param oldName the current name of the key to update + * @param newName the new name to assign to the key + * @return {@code true} if the key name was successfully updated, {@code false} if no matching key was found + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ + public static synchronized boolean changeKeyNameByName(String oldName, String newName) throws SQLException { String sql = "UPDATE keys SET name = ? WHERE name = ?"; @@ -253,6 +288,18 @@ public static synchronized boolean changeKeyNameByName(String oldName, String ne } return false; } + /** + * Updates the role of a key in the {@code keys} table based on its name. + * + *This method changes the {@code role} associated with a key identified by its {@code name}. + * It is often used to modify permissions or access levels dynamically.
+ * + * @param name the name of the key whose role should be updated + * @param newRole the new {@link Role} to assign to the key + * @return {@code true} if the role was successfully updated, {@code false} otherwise + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ + public static synchronized boolean changeKeyRoleByName(String name, Role newRole) throws SQLException { String sql = "UPDATE keys SET role = ? WHERE name = ?"; @@ -269,6 +316,18 @@ public static synchronized boolean changeKeyRoleByName(String name, Role newRole } return false; } + /** + * Updates the role of a key in the {@code keys} table based on its authentication value. + * + *This method locates a key using its {@code value} (typically an authentication token) + * and assigns it a new {@link Role}. It is useful for adjusting access permissions tied + * directly to API keys or tokens.
+ * + * @param val the authentication value of the key whose role should be changed + * @param newRole the new {@link Role} to assign + * @return {@code true} if the key's role was updated, {@code false} if no matching key was found + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ public static synchronized boolean changeKeyRoleByAuth(String val, Role newRole) throws SQLException { String sql = "UPDATE keys SET role = ? WHERE value = ?"; @@ -287,6 +346,18 @@ public static synchronized boolean changeKeyRoleByAuth(String val, Role newRole) return false; } + /** + * Updates the stored authentication value of a key based on its name. + * + *This method identifies a key by its {@code name} and updates the {@code value} + * field (typically representing the authentication token). It is used when rotating + * or regenerating key values.
+ * + * @param value the current name of the key to update + * @param newName the new authentication value to assign to the key + * @return {@code true} if the key value was successfully updated, {@code false} otherwise + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ public static synchronized boolean changeKeyNameByAuth(String value, String newName) throws SQLException { String sql = "UPDATE keys SET value = ? WHERE name = ?"; diff --git a/src/main/java/upr/famnit/managers/connections/Management.java b/src/main/java/upr/famnit/managers/connections/Management.java index 00ada48..94ff225 100644 --- a/src/main/java/upr/famnit/managers/connections/Management.java +++ b/src/main/java/upr/famnit/managers/connections/Management.java @@ -145,7 +145,7 @@ private void handleKeyRoute() throws IOException, SQLException { case "GET" -> handleListKeysRequest(); case "POST" -> handleInsertKeyRequest(); case "DELETE" -> handleDeleteKeyRequest(); - case "UPDATE" -> handleKeyChangeReq(); + case "PATCH" -> handleKeyChangeReq(); case null, default -> respond(ResponseFactory.NotFound()); } } @@ -437,23 +437,61 @@ private void handleListKeysRequest() throws IOException { respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); } + /** + * Handles a request to modify an existing key's properties such as its name or role. + * + *This method processes an incoming JSON request from the client containing one or more + * modification instructions. Depending on the provided fields, the method can update a key's: + *
The request must include an authentication value ({@code auth}) belonging to a key with + * {@link Role#Admin} privileges; otherwise, the operation is rejected.
+ * + *Valid update combinations:
+ *Depending on the requested changes, this method delegates updates to the appropriate + * {@link DatabaseManager} methods, sends an HTTP response via {@link ResponseFactory}, + * and logs the operation outcome.
+ * + * @throws IOException if reading the request body or writing the response fails + * @throws SQLException if a database operation triggered by the update fails + */ + private void handleKeyChangeReq() throws IOException, SQLException { + String authHeader = clientRequest.getRequest().getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + respond(ResponseFactory.BadRequest()); + return; + } + + String token = authHeader.substring("Bearer ".length()).trim(); + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject(); - String authValue = jsonObject.get("auth").getAsString(); - Key requester = DatabaseManager.getKeyByValue(authValue); + Key requester = DatabaseManager.getKeyByValue(token); boolean update = false; if (requester == null || (requester.getRole()) != Role.Admin) { respond(ResponseFactory.MethodNotAllowed()); return; } - String auth = jsonObject.has("value") ? jsonObject.get("value").getAsString().trim() : ""; - String name = jsonObject.has("name") ? jsonObject.get("name").getAsString().trim() : ""; - String newRole = jsonObject.has("roleNew") ? jsonObject.get("roleNew").getAsString().trim() : ""; - String newName = jsonObject.has("newName") ? jsonObject.get("newName").getAsString().trim() : ""; + String auth = getJsonString(jsonObject, "value"); + String name = getJsonString(jsonObject, "name"); + String newRole = getJsonString(jsonObject, "roleNew"); + String newName = getJsonString(jsonObject, "newName"); + boolean isnewName = !newName.isEmpty(); boolean hasNewRole = !newRole.isEmpty(); boolean bothChange = isnewName && hasNewRole; @@ -464,13 +502,12 @@ private void handleKeyChangeReq() throws IOException, SQLException { if (bothChange) { update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); - respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); update = DatabaseManager.changeKeyNameByAuth(auth, newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); Logger.log("Change of name and role succesfull", LogLevel.success); return; } else if (isnewName) { - update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); + update = DatabaseManager.changeKeyNameByAuth(auth,newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); return; @@ -485,13 +522,12 @@ private void handleKeyChangeReq() throws IOException, SQLException { } else if (!name.isEmpty() && auth.isEmpty()) { if (bothChange) { update = DatabaseManager.changeKeyRoleByName(name, Role.fromString(newRole)); - respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); update = DatabaseManager.changeKeyNameByName(name, newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); Logger.log("Change of name and role succesfull", LogLevel.success); return; } else if (isnewName) { - update = DatabaseManager.changeKeyRoleByName(name, Role.fromString(newRole)); + update = DatabaseManager.changeKeyNameByName(name,newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); return; @@ -550,4 +586,10 @@ private boolean isAdminRequest() { private void respond(Response response) throws IOException { StreamUtil.sendResponse(clientRequest.getClientSocket().getOutputStream(), response); } + private String getJsonString(JsonObject obj, String key) { + return obj.has(key) && !obj.get(key).isJsonNull() + ? obj.get(key).getAsString().trim() + : ""; + } + } From f75006fecf6e42abf0718ade569f9031e2ebd5f2 Mon Sep 17 00:00:00 2001 From: FloksterIf a connection already exists and is open, it returns the existing connection. - * Otherwise, it creates a new connection and returns it.
+ *This method follows a singleton pattern: if a connection already exists and is open, + * it reuses that connection. Otherwise, it creates a new connection to the database.
+ * + *It also configures SQLite to use WAL (Write-Ahead Logging) mode, which allows: + *
The {@code busy_timeout} PRAGMA is also set to 2000ms (2 seconds), which tells SQLite + * to wait up to 2 seconds for a locked database to become available instead of immediately + * throwing an SQLITE_BUSY exception.
+ *The timeout is to avoid SQL DB lockout
+ * + *Overall, this setup is essential for multithreaded applications that share a single + * SQLite database connection, like your server handling multiple POST requests simultaneously.
* * @return the {@link Connection} object for interacting with the database * @throws SQLException if a database access error occurs or the URL is invalid */ + public static Connection connect() throws SQLException { if (connection == null || connection.isClosed()) { connection = DriverManager.getConnection(DATABASE_URL); Logger.info("Database connection established."); } + + try (Statement stmt = connection.createStatement()) { + stmt.execute("PRAGMA journal_mode=WAL;"); + stmt.execute("PRAGMA synchronous=NORMAL;"); + stmt.execute("PRAGMA busy_timeout = 2000;"); + } + + Logger.info("Database connection established."); return connection; } @@ -76,29 +101,35 @@ public static synchronized void createKeysTable() throws SQLException { } /** - * Creates the {@code AllowedModelsTable} table in the database if it does not already exist. + * Creates the {@code blocked_models} table in the database if it does not already exist. * - *The {@code keys} table stores information about authentication keys, including their - * unique identifier, name, value, and associated role. This method ensures that the - * table structure is in place before any key-related operations are performed.
+ *The {@code blocked_models} table stores model usage restrictions for each key. + * Each entry links a key (via its {@code key_id}) to a model name that the key is + * not allowed to access. This allows fine-grained control over which models a user + * or API key can use.
+ * + *The table uses a composite primary key consisting of {@code key_id} and + * {@code model_name} to ensure that the same model cannot be blocked twice for the + * same key. A foreign key constraint references the {@code keys} table, ensuring that + * restrictions are automatically removed if a key is deleted.
* * @throws SQLException if a database access error occurs or the SQL statement is invalid */ - /*public static synchronized void createAllowedModelsTable() throws SQLException { - String sql = "CREATE TABLE IF NOT EXISTS allowed_models (\n" + + public static synchronized void createBlockedModelsTable() throws SQLException { + String sql = "CREATE TABLE IF NOT EXISTS blocked_models (\n" + " key_value TEXT NOT NULL,\n" + " model_name TEXT NOT NULL,\n" - + " PRIMARY KEY (key_value, model_name)\n" + + " PRIMARY KEY (key_value, model_name),\n" + + " FOREIGN KEY (key_value) REFERENCES keys(value) ON DELETE CASCADE\n" + ");"; try (Connection conn = connect(); Statement statement = conn.createStatement()) { statement.execute(sql); - Logger.info("Allowed models table created or already exists."); + Logger.info("Blocked models table created or already exists."); } } - */ - /** * Inserts a new {@link Key} into the {@code keys} table. @@ -375,8 +406,209 @@ public static synchronized boolean changeKeyNameByAuth(String value, String newN } return false; } + /** + * Blocks access to a specific model for the given key in the {@code blocked_models} table. + * + *This method first retrieves the key's role from the {@code keys} table. If the key + * has a role of {@code admin}, the operation is not allowed and an {@link IllegalStateException} + * is thrown. This ensures that admin keys always retain access to all models.
+ * + *If the key exists and is not an admin, a new entry is inserted into the + * {@code blocked_models} table linking the {@code key_id} with the {@code model_name}. + * This effectively prevents the key from using the specified model.
+ * + *Logging is performed at each stage: + *
This method allows previously blocked models to be unblocked for a specific key. + * It first checks if the key exists in the {@code keys} table. If the key does not exist, + * an {@link IllegalStateException} is thrown.
+ * + *Logging is performed at each stage: + *
This method queries the {@code blocked_models} table to determine if the key + * has a restriction for the given model. If there is no entry in {@code blocked_models} + * for the key and model, the key is allowed to use it.
+ * + *Logging is performed at each stage: + *
This method queries all models from the {@code models} table and excludes any + * that are blocked for the given key in the {@code blocked_models} table. The result + * is a list of models the key can actually access.
+ * + *Logging is performed: + *
allowed_models table with columns:
+ * key_value (TEXT, NOT NULL)model_name (TEXT, NOT NULL)(key_value, model_name).key_value to the keys table,
+ * with cascading delete.This method is synchronized to prevent concurrent creation attempts from multiple threads.
+ * + * @throws SQLException If a database access error occurs while creating the table. + */ + + public static synchronized void createWhiteListTable() throws SQLException{ + String sql = "CREATE TABLE IF NOT EXISTS allowed_models (\n" + + " key_value TEXT NOT NULL,\n" + + " model_name TEXT NOT NULL,\n" + + " PRIMARY KEY (key_value, model_name),\n" + + " FOREIGN KEY (key_value) REFERENCES keys(value) ON DELETE CASCADE\n" + + ");"; + try (Connection conn = connect(); Statement statement = conn.createStatement()){ + statement.execute(sql); + Logger.info("Allowed models table created"); + } + } + /** * Inserts a new {@link Key} into the {@code keys} table. @@ -308,12 +345,12 @@ public static synchronized boolean changeKeyNameByName(String oldName, String ne try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, newName); - stmt.setString(2, oldName); + stmt.setString (1, newName); + stmt.setString (2, oldName); int affectedRows = stmt.executeUpdate(); if (affectedRows > 0) { - Logger.info("Key name updated: " + oldName + " → " + newName); + Logger.info ("Key name updated: " + oldName + " → " + newName); return true; } } @@ -360,7 +397,7 @@ public static synchronized boolean changeKeyRoleByName(String name, Role newRole * @throws SQLException if a database access error occurs or the SQL statement is invalid */ - public static synchronized boolean changeKeyRoleByAuth(String val, Role newRole) throws SQLException { + public static synchronized boolean changeKeyRoleByValue(String val, Role newRole) throws SQLException { String sql = "UPDATE keys SET role = ? WHERE value = ?"; try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -390,7 +427,7 @@ public static synchronized boolean changeKeyRoleByAuth(String val, Role newRole) * @throws SQLException if a database access error occurs or the SQL statement is invalid */ - public static synchronized boolean changeKeyNameByAuth(String value, String newName) throws SQLException { + public static synchronized boolean changeKeyNameByValue(String value, String newName) throws SQLException { String sql = "UPDATE keys SET value = ? WHERE name = ?"; try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -531,7 +568,7 @@ public static synchronized void unblockModelForKey(String keyValue, String model * @throws SQLException if a database access error occurs or the SQL statement is invalid * @throws IllegalStateException if the key does not exist */ - public static synchronized boolean canKeyUseModel(String keyValue, String modelName) throws SQLException { + public static synchronized boolean canKeyUseModelBlack(String keyValue, String modelName) throws SQLException { // Check if the key exists String keyCheckSql = "SELECT value FROM keys WHERE value = ?"; try (Connection conn = connect(); PreparedStatement checkStmt = conn.prepareStatement(keyCheckSql)) { @@ -563,6 +600,60 @@ public static synchronized boolean canKeyUseModel(String keyValue, String modelN return allowed; } } + + /** + * Checks whether a given key is allowed to use a specific model. + * + *This method queries the {@code blocked_models} table to determine if the key + * has a restriction for the given model. If there is no entry in {@code blocked_models} + * for the key and model, the key is allowed to use it.
+ * + *Logging is performed at each stage: + *
This method first retrieves the key's role from the {@code keys} table. If the key + * has a role of {@code admin}, the operation is not allowed and an {@link IllegalStateException} + * is thrown. This ensures that admin keys always retain access to all models.
+ * + *If the key exists and is not an admin, a new entry is inserted into the + * {@code allowed_modles} table linking the {@code key_value} with the {@code model_name}. + * This effectively ensures the key is able to use this model.
+ * + *Logging is performed at each stage: + *
This method allows previously allowed models to be blocked for a specific key. + * It first checks if the key exists in the {@code keys} table. If the key does not exist, + * an {@link IllegalStateException} is thrown.
+ * + *Logging is performed at each stage: + *
This method queries the {@code allowed_models} table to determine if the key + * has a restriction for the given model. If there is an entry in {@code allowed_models} + * for the key and model, the key is allowed to use it.
+ * + *Logging is performed at each stage: + *
This method queries all models from the {@code models} table and excludes any + * that are blocked for the given key in the {@code blocked_models} table. The result + * is a list of models the key can actually access.
+ * + *Logging is performed: + *
This method performs the following actions: + *
GET, it retrieves all blocked models via {@link #getAllBlock()}.POST, it inserts new blocked models via {@link #insertModelBlock()}.DELETE, it removes blocked models via {@link #deleteModelBlock()}.This method performs the following actions: + *
GET, it retrieves all allowed models via {@link #getAllAllowed()}.POST, it inserts new allowed models via {@link #insertAllowance()}.DELETE, it removes allowed models via {@link #deleteAllowedModel()}.This method performs the following actions: + *
If an SQL or I/O error occurs during processing, the method logs the exception + * and returns without performing additional actions.
+ * + * @throws IOException If an error occurs while reading the request body or sending a response. + * @throws SQLException If a database access error occurs during authorization or model deletion. + */ + + private void deleteAllowedModel() throws IOException, SQLException { + + String authHeader = clientRequest.getRequest().getHeader("Authorization"); String token = authHeader.substring("Bearer ".length()).trim(); Key requester = DatabaseManager.getKeyByValue(token); - if (requester == null) { - respond(ResponseFactory.MethodNotAllowed()); - return; - } - if(!isAdminRequest()){ - respond(ResponseFactory.BadRequest()); - Logger.log("Only admin can Delete allowed models"); + if(!getAuthorization(authHeader,requester)){ return; } + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); JsonObject jsonObject = null; if (!body.isEmpty()) { @@ -235,21 +285,31 @@ private void deleteModelBlock() throws IOException, SQLException { if(jsonObject.has("targetKeyValue")){ String targetKeyValue=jsonObject.get("targetKeyValue").getAsString(); Key targetKey=DatabaseManager.getKeyByValue(String.valueOf(targetKeyValue)); - if(jsonObject.has("blockedModels")&&jsonObject.get("blockedModels").isJsonNull()){ + if(jsonObject.has("modelName")&&jsonObject.get("modelName").isJsonNull()){ respond(ResponseFactory.BadRequest()); Logger.log("No models to delete from table",LogLevel.error); return; } - String allowedModelStr=jsonObject.get("blockedModels").getAsString(); + boolean allGood=false; + String allowedModelStr=jsonObject.get("modelName").getAsString(); ArrayListThis method performs the following actions: + *
If a database or I/O exception occurs at any point, the method logs the issue and + * terminates processing without completing the insert operations.
+ * + * @throws IOException If an error occurs while reading the request body or sending a response. + * @throws SQLException If a database error occurs while allowing models for a key. + */ + + private void insertAllowance() throws IOException, SQLException { + String authHeader = clientRequest.getRequest().getHeader("Authorization"); - // Validate Authorization header - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - respond(ResponseFactory.BadRequest()); + + String token = authHeader.substring("Bearer ".length()).trim(); + + // Get the key from the token + Key requester = DatabaseManager.getKeyByValue(token); + + + if(!getAuthorization(authHeader,requester)){ return; } + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); + JsonObject jsonObject = null; + if (!body.isEmpty()) { + jsonObject = JsonParser.parseString(body).getAsJsonObject(); + }try { + + if(jsonObject != null&&jsonObject.has("targetKeyValue")){ + String targetKeyValue=jsonObject.get("targetKeyValue").getAsString(); + //Key targetKey=DatabaseManager.getKeyByValue(targetKeyValue); + if(!jsonObject.has("modelName")&&jsonObject.get("modelName").isJsonNull()){ + respond(ResponseFactory.BadRequest()); + Logger.log("No models to add to table",LogLevel.error); + return; + } + String allowedModelStr=jsonObject.get("modelName").getAsString(); + ArrayListThis method performs the following actions: + *
targetKeyValue.If a database error occurs during processing, the method sends an internal server error + * response and logs the exception.
+ * + * @throws IOException If an error occurs while reading the request body or sending the response. + * @throws SQLException If a database access error occurs during authorization or model retrieval. + */ + + private void getAllAllowed() throws IOException, SQLException { + String authHeader = clientRequest.getRequest().getHeader("Authorization"); + + // Validate Authorization header + String token = authHeader.substring("Bearer ".length()).trim(); // Get the key from the token Key requester = DatabaseManager.getKeyByValue(token); - if (requester == null) { - respond(ResponseFactory.MethodNotAllowed()); + if ( !getAuthorization (authHeader, requester)){ return; } - if(!isAdminRequest()){ - respond(ResponseFactory.BadRequest()); - Logger.log("Only admin can insert allowed models"); + + boolean isAdmin = isAdminRequest(); + Key targetKey; + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); + JsonObject jsonObject = null; + if (!body.isEmpty()) { + jsonObject = JsonParser.parseString(body).getAsJsonObject(); + } + + try { + if(jsonObject.has("targetKeyValue")){ + String targetKeyValue=jsonObject.get("targetKeyValue").getAsString(); + if(!targetKeyValue.equals(token)&&requester.getRole()!=Role.Admin){ + respond(ResponseFactory.MethodNotAllowed()); + Logger.log("Unauthorized attempt to view another key's blocked models", LogLevel.warn); + Logger.log("Admin can view all, others cannot"); + return; + } + //Check target key de se prepričam de obstaja + targetKey=DatabaseManager.getKeyByValue(targetKeyValue); + if (targetKey==null){ + respond(ResponseFactory.BadRequest()); + Logger.log("Target key not found: "+targetKeyValue,LogLevel.error); + return; + } + //Nucam ArrayList de lažje loopam skoz object pa tud za response generation + ArrayListThis method performs the following actions: + *
targetKeyValue and models to unblock.modelName field exists and is not null.If no models were successfully unblocked or an error occurs, the method responds
+ * with a 400 Bad Request and logs the issue. Database or I/O exceptions
+ * are logged without further retries.
This method performs the following actions: + *
targetKeyValue and modelName) are present and non-null.If a database exception occurs during model insertion, the method logs the error and + * terminates processing without completing the full update.
+ * + * @throws IOException If an error occurs while reading the request body or sending a response. + * @throws SQLException If a database error occurs while blocking models for a key. + */ + + private void insertModelBlock() throws IOException, SQLException { + String authHeader = clientRequest.getRequest().getHeader("Authorization"); + + String token = authHeader.substring("Bearer ".length()).trim(); + + // Get the key from the token + Key requester = DatabaseManager.getKeyByValue(token); + + if(!getAuthorization(authHeader,requester)){ return; } String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); @@ -292,12 +585,12 @@ private void insertModelBlock() throws IOException, SQLException { if(jsonObject != null&&jsonObject.has("targetKeyValue")){ String targetKeyValue=jsonObject.get("targetKeyValue").getAsString(); //Key targetKey=DatabaseManager.getKeyByValue(targetKeyValue); - if(!jsonObject.has("blockedModels")&&jsonObject.get("blockedModels").isJsonNull()){ + if(!jsonObject.has("modelName")&&jsonObject.get("modelName").isJsonNull()){ respond(ResponseFactory.BadRequest()); Logger.log("No models to add to table",LogLevel.error); return; } - String allowedModelStr=jsonObject.get("blockedModels").getAsString(); + String allowedModelStr=jsonObject.get("modelName").getAsString(); ArrayListThis method performs the following actions: + *
targetKeyValue.If a database error occurs during processing, the method logs the exception and + * responds with an internal server error.
+ * + * @throws IOException If an error occurs while reading the request body or sending the response. + * @throws SQLException If a database access error occurs during model retrieval. + */ + private void getAllBlock() throws IOException,SQLException { String authHeader = clientRequest.getRequest().getHeader("Authorization"); // Validate Authorization header - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - respond(ResponseFactory.BadRequest()); - return; - } String token = authHeader.substring("Bearer ".length()).trim(); // Get the key from the token Key requester = DatabaseManager.getKeyByValue(token); - if (requester == null) { - respond(ResponseFactory.MethodNotAllowed()); + if ( !getAuthorization (authHeader, requester)){ return; } + boolean isAdmin = isAdminRequest(); Key targetKey; String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); @@ -358,7 +669,7 @@ private void getAllBlock() throws IOException,SQLException { return; } //Nucam ArrayList de lažje loopam skoz object pa tud za response generation - ArrayListThis method performs the following actions: + *
POST, it delegates processing to
+ * {@link #handleWorkersCommandRequest()}.404 Not Found status.This routing method ensures that only valid worker-command operations are processed, + * while invalid methods are rejected in a consistent and predictable way.
+ * + * @throws IOException If an error occurs while sending the HTTP response. + */ + private void handleWorkerCommandRoute() throws IOException { switch (clientRequest.getRequest().getMethod()) { case "POST" -> handleWorkersCommandRequest(); @@ -394,6 +723,26 @@ private void handleWorkerCommandRoute() throws IOException { } } + /** + * Handles an incoming worker command request by parsing the request body, + * logging the command, and forwarding it to the Overseer for execution. + * + *This method performs the following actions: + *
This method does not perform authentication or validation—such checks + * are expected to occur earlier in the request-handling pipeline.
+ * + * @throws IOException If an error occurs while reading the request body or sending the response. + */ + private void handleWorkersCommandRequest() throws IOException { Gson gson = new GsonBuilder().create(); String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); @@ -532,12 +881,11 @@ private void handleDeleteKeyRequest() throws IOException, SQLException { String authValue = jsonObject.get("auth").getAsString(); Key requester = DatabaseManager.getKeyByValue(authValue); - - if (requester == null || (requester.getRole()) != Role.Admin) { - respond(ResponseFactory.MethodNotAllowed()); + String authHeader = clientRequest.getRequest().getHeader("Authorization"); + String token = authHeader.substring("Bearer ".length()).trim(); + if(!getAuthorization(authHeader,requester)){ return; } - String value = jsonObject.has("value") ? jsonObject.get("value").getAsString().trim() : ""; String name = jsonObject.has("name") ? jsonObject.get("name").getAsString().trim() : ""; Key key = null; @@ -559,7 +907,7 @@ private void handleDeleteKeyRequest() throws IOException, SQLException { Logger.log("Key deleted by value: " + key.getValue(), LogLevel.success); respond(ResponseFactory.Ok(value.getBytes())); return; - } else if (!deleteByValue && key.getRole() != Role.Admin) { + } else if (key.getRole() != Role.Admin) { // Key exists and is NOT Admin → delete it Logger.log(key.toString()); boolean deleted = DatabaseManager.deleteKeyByName(key.getName()); @@ -645,25 +993,20 @@ private void handleListKeysRequest() throws IOException { private void handleKeyChangeReq() throws IOException, SQLException { String authHeader = clientRequest.getRequest().getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - respond(ResponseFactory.BadRequest()); + String token = authHeader.substring("Bearer ".length()).trim(); + Key requester = DatabaseManager.getKeyByValue(token); + if(!getAuthorization(authHeader,requester)){ return; } - String token = authHeader.substring("Bearer ".length()).trim(); String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject(); - Key requester = DatabaseManager.getKeyByValue(token); boolean update = false; - if (requester == null || (requester.getRole()) != Role.Admin) { - respond(ResponseFactory.MethodNotAllowed()); - return; - } + String auth = getJsonString(jsonObject, "value"); String name = getJsonString(jsonObject, "name"); String newRole = getJsonString(jsonObject, "roleNew"); @@ -678,18 +1021,18 @@ private void handleKeyChangeReq() throws IOException, SQLException { if (!auth.isEmpty() && name.isEmpty()) { if (bothChange) { - update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); - update = DatabaseManager.changeKeyNameByAuth(auth, newName); + update = DatabaseManager.changeKeyRoleByValue(auth, Role.fromString(newRole)); + update = DatabaseManager.changeKeyNameByValue(auth, newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); Logger.log("Change of name and role succesfull", LogLevel.success); return; } else if (isnewName) { - update = DatabaseManager.changeKeyNameByAuth(auth,newName); + update = DatabaseManager.changeKeyNameByValue(auth,newName); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); return; } else if (hasNewRole) { - update = DatabaseManager.changeKeyRoleByAuth(auth, Role.fromString(newRole)); + update = DatabaseManager.changeKeyRoleByValue(auth, Role.fromString(newRole)); respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); return; } @@ -763,14 +1106,103 @@ private boolean isAdminRequest() { private void respond(Response response) throws IOException { StreamUtil.sendResponse(clientRequest.getClientSocket().getOutputStream(), response); } + + /** + * Safely retrieves a string value from a {@link JsonObject} for the given key. + * + *This method checks whether the JSON object contains the specified key and that
+ * the associated value is not null. If the value exists, it is returned
+ * as a trimmed string. If the key is missing or the value is null,
+ * an empty string is returned.
null.
+ */
+
private String getJsonString(JsonObject obj, String key) {
return obj.has(key) && !obj.get(key).isJsonNull()
? obj.get(key).getAsString().trim()
: "";
}
- private ArrayListThis method simply delegates to the {@link DatabaseManager} to fetch + * the list of blocked models associated with the provided key value.
+ * + * @param keyValue The value of the key whose blocked models should be retrieved. + * @return An {@link ArrayList} containing the names of all blocked models. + * + * @throws SQLException If a database error occurs while retrieving the models. + */ + private ArrayListThis method delegates to the {@link DatabaseManager} to obtain the list of + * allowed models associated with the given key value.
+ * + * @param keyValue The value of the key whose allowed models should be retrieved. + * @return An {@link ArrayList} containing the names of all allowed models. + * + * @throws SQLException If a database error occurs while retrieving the models. + */ + private ArrayListThis method performs the following actions: + *
If validation succeeds, the method returns true, allowing the caller
+ * to proceed with the requested operation.
true if authorization is valid and the requester is an admin;
+ * false otherwise.
+ *
+ * @throws IOException If an error occurs while sending an error response.
+ */
+
+ private boolean getAuthorization(String authHeader,Key requester) throws IOException {
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ respond(ResponseFactory.BadRequest());
+ return false;
+ }
+
+ String token = authHeader.substring("Bearer ".length()).trim();
+
+ //
+
+ if (requester == null) {
+ respond(ResponseFactory.MethodNotAllowed());
+ return false;
+ }
+ if(!isAdminRequest()){
+ respond(ResponseFactory.BadRequest());
+ Logger.log("Only admin can insert allowed models");
+ return false;
+ }
+ return true;
}
From 2706b91f6144d46926cb34423bc938e3377c6205 Mon Sep 17 00:00:00 2001
From: Flokster This expects the request body to contain a JSON key named {@code "model"}. + * If the key is missing or unreadable, {@code null} is returned.
+ * + * @param request the {@link ClientRequest} whose model should be extracted + * @return the model name, or {@code null} if not found + */ + public static String extractModel(ClientRequest request) { + return StreamUtil.getValueFromJSONBody("model", request.getRequest().getBody()); + } + + /** + * Extracts a Bearer token from the request's {@code Authorization} header. + * + *The header must be in the format:
+ *+ * Authorization: Bearer <token> + *+ * + *
If the header is missing, malformed, or does not begin with "Bearer ", + * {@code null} is returned.
+ * + * @param request the {@link ClientRequest} containing the Authorization header + * @return the extracted token, or {@code null} if not present or invalid + */ + public static String extractToken(ClientRequest request) { + String auth = request.getRequest().getHeaders().get("Authorization"); + if (auth == null) return null; + if (!auth.startsWith("Bearer ")) return null; + return auth.substring("Bearer ".length()).trim(); + } + + /** + * Determines whether a given token is allowed to access a specific model. + * + *The permission rules are as follows:
+ * + *This method fetches the allow- and block-lists from the database on each call.
+ * + * @param model the model being requested + * @param token the client's authentication token + * @return {@code true} if access is permitted; {@code false} otherwise + * @throws SQLException if permission data cannot be retrieved + */ + private static boolean isAllowedForModel(String model, String token) throws SQLException { + // TODO: call your real permission system + ArrayListThe permission rules are as follows:
+ *Permission rules (evaluated in order of precedence):
* - *This method fetches the allow- and block-lists from the database on each call.
+ *This method retrieves exclusivity, allow-list, and block-list data + * from the database on each invocation.
+ * + * @param keyValue the key requesting access to the model + * @param modelName the model being requested * - * @param model the model being requested - * @param token the client's authentication token * @return {@code true} if access is permitted; {@code false} otherwise - * @throws SQLException if permission data cannot be retrieved + * + * @throws SQLException if permission or exclusivity data cannot be retrieved */ - private static boolean isAllowedForModel(String model, String token) throws SQLException { - // TODO: call your real permission system - ArrayListThis method queries the {@code blocked_models} table to determine if the key - * has a restriction for the given model. If there is no entry in {@code blocked_models} - * for the key and model, the key is allowed to use it.
+ * Determines whether a key is permitted to use a given model by consulting the database. * - *Logging is performed at each stage: + *
This method enforces the following policy:
*Note: despite the table name {@code allowed_models}, the current logic uses it as a + * block list (presence = blocked, absence = allowed). If this is not intended, + * the SQL/boolean logic should be inverted or the table renamed to reflect its role.
* - * @return {@code true} if the key can use the model, {@code false} if blocked - * @throws SQLException if a database access error occurs or the SQL statement is invalid - * @throws IllegalStateException if the key does not exist + * @param keyValue the key identifier from the {@code keys} table + * @param modelName the model name to check + * @return {@code true} if the key is allowed to use the model; {@code false} if it is blocked + * @throws SQLException if a database access error occurs or the SQL is invalid + * @throws IllegalStateException if the key does not exist in {@code keys} */ public static synchronized boolean canKeyUseModelWhiteDataBase(String keyValue, String modelName) throws SQLException { // Check if the key exists @@ -642,7 +663,7 @@ public static synchronized boolean canKeyUseModelWhiteDataBase(String keyValue, ResultSet rs = stmt.executeQuery(); rs.next(); - boolean allowed = rs.getInt("count") == 0; + boolean allowed = rs.getInt("count") > 0; if (allowed) { Logger.info("Key ID " + keyValue + " is allowed to use model '" + modelName + "'."); @@ -841,14 +862,14 @@ public static synchronized boolean canKeyUseModelWhite(String keyValue, String m } // Check if the model is blocked - String sql = "SELECT COUNT(*) AS count FROM blocked_models WHERE key_value = ? AND model_name = ?"; + String sql = "SELECT COUNT(*) AS count FROM allowed_models WHERE key_value = ? AND model_name = ?"; try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setString(1, keyValue); stmt.setString(2, modelName); ResultSet rs = stmt.executeQuery(); rs.next(); - boolean allowed = rs.getInt("count") == 0; + boolean allowed = rs.getInt("count") > 0; if (allowed) { Logger.info("Key ID " + keyValue + " is allowed to use model '" + modelName + "'."); @@ -906,5 +927,234 @@ public static synchronized ArrayListAuthorization: Only keys with the {@code admin} role may call this method.
+ * + *Behavior: + *
This operation is atomic and enforced at the database level.
+ * + * @param requestingKey the admin key performing the operation + * @param targetKey the key that will gain exclusive access to the model + * @param modelName the model to be made exclusive + * + * @throws SecurityException if {@code requestingKey} is not an admin key + * @throws SQLException if a database error occurs + */ + public static synchronized void setExclusiveModelForKey(String requestingKey, String targetKey, String modelName) throws SQLException { + + if (!isAdminKey(requestingKey)) { + throw new SecurityException("Only admin keys can assign exclusive models."); + } + + String sql = """ + INSERT INTO model_exclusive (model_name, key_value) + VALUES (?, ?) + ON CONFLICT(model_name) + DO UPDATE SET key_value = excluded.key_value + """; + + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, modelName); + stmt.setString(2, targetKey); + stmt.executeUpdate(); + } + } + + /** + * Removes exclusive access from a model. + * + *Authorization: Only keys with the {@code admin} role may call this method.
+ * + *If the model is not currently exclusive, this method performs no changes.
+ * + * @param requestingKey the admin key performing the removal + * @param modelName the model whose exclusivity should be removed + * + * @throws SecurityException if {@code requestingKey} is not an admin key + * @throws SQLException if a database error occurs + */ + public static synchronized void removeExclusiveModel(String requestingKey, String modelName) throws SQLException { + + // 🔒 Authorization check + if (!isAdminKey(requestingKey)) { + throw new SecurityException("Only admin keys can remove model exclusivity."); + } + + String sql = "DELETE FROM model_exclusive WHERE model_name = ?"; + + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, modelName); + int rows = stmt.executeUpdate(); + + if (rows > 0) { + Logger.info("Admin key " + requestingKey + + " removed exclusivity for model " + modelName); + } else { + Logger.warn("Model '" + modelName + "' had no exclusivity set."); + } + } + } + + /** + * Determines whether a key is the exclusive owner of a given model. + * + *This method checks global exclusivity only. + * It does not consider allow or block lists.
+ * + *A model is considered usable by a key if: + *
This method does not perform authorization checks and is intended + * for administrative or informational use.
+ * + * @param keyValue the key whose exclusive models should be returned + * + * @return a list of model names exclusively owned by the key; + * the list is empty if none exist + * + * @throws SQLException if a database error occurs + */ + public static synchronized ArrayListIf the model is not exclusive, this method returns {@code null}.
+ * + * @param modelName the model to check + * + * @return the key that owns the model exclusively, or {@code null} + * if the model is not exclusive + * + * @throws SQLException if a database error occurs + */ + + public static synchronized String getExclusiveOwnerForModel(String modelName) + throws SQLException { + + String sql = "SELECT key_value FROM model_exclusive WHERE model_name = ?"; + + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, modelName); + ResultSet rs = stmt.executeQuery(); + + return rs.next() ? rs.getString("key_value") : null; + } + } + + /** + * Determines whether a key has administrative privileges. + * + *A key is considered an admin if its role in the {@code keys} table + * is {@code "admin"} (case-insensitive).
+ * + * @param keyValue the key to check + * + * @return {@code true} if the key has admin privileges, + * {@code false} otherwise + * + * @throws SQLException if a database error occurs + */ + + public static boolean isAdminKey(String keyValue) throws SQLException { + String sql = "SELECT role FROM keys WHERE value = ?"; + + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, keyValue); + ResultSet rs = stmt.executeQuery(); + + return rs.next() && "admin".equalsIgnoreCase(rs.getString("role")); + } + } + + + + } + diff --git a/src/main/java/upr/famnit/managers/connections/Client.java b/src/main/java/upr/famnit/managers/connections/Client.java index 47ff703..7960d7f 100644 --- a/src/main/java/upr/famnit/managers/connections/Client.java +++ b/src/main/java/upr/famnit/managers/connections/Client.java @@ -1,6 +1,7 @@ package upr.famnit.managers.connections; import upr.famnit.components.*; +import upr.famnit.util.LogLevel; import upr.famnit.util.Logger; import upr.famnit.util.StreamUtil; @@ -77,7 +78,6 @@ public void run() { Logger.error("Failed reading client request (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); return; } - try { if (!RequestQue.addTask(cr)) { Logger.error("Closing request due to invalid structure (" + clientSocket.getRemoteSocketAddress() + ")"); @@ -95,7 +95,8 @@ public void run() { cr.getRequest().log(); } } catch (SQLException e) { - throw new RuntimeException(e); + Logger.log("Rejecting request because the database failed from:" +clientSocket.getRemoteSocketAddress()+ " "+e.getMessage() ); + rejectRequest(clientSocket); } } @@ -115,4 +116,25 @@ private void rejectRequest(Socket clientSocket) { Logger.error("Could not send rejection response to socket: " + clientSocket.getInetAddress()); } } + + private void rejectInvalidRequest(ClientRequest cr) { + Logger.error("Closing request due to invalid structure (" + + cr.getClientSocket().getRemoteSocketAddress() + ")"); + Response failed = ResponseFactory.MethodNotAllowed(); + + try { + StreamUtil.sendResponse(cr.getClientSocket().getOutputStream(), failed); + } catch (IOException e) { + Logger.error("Unable to respond to client: " + e.getMessage()); + } + + try { + cr.getClientSocket().close(); + } catch (IOException e) { + Logger.error("Unable to close client socket: " + e.getMessage()); + } + + cr.getRequest().log(); + } + } diff --git a/src/main/java/upr/famnit/managers/connections/Management.java b/src/main/java/upr/famnit/managers/connections/Management.java index 3362374..05e3848 100644 --- a/src/main/java/upr/famnit/managers/connections/Management.java +++ b/src/main/java/upr/famnit/managers/connections/Management.java @@ -106,6 +106,7 @@ public void run() { case "/queue" -> handleQueueRoute(); case "/block" -> handleWorkerBlockRoute(); case "/allow"->handleWorkerWhiteList(); + case "/exclusive"->handleExclusiveList(); case null, default -> respond(ResponseFactory.NotFound()); } @@ -118,6 +119,284 @@ public void run() { } + /** + * Routes requests for the exclusive list endpoint based on HTTP method. + *+ * Supported methods: + *
+ * Behavior: + *
+ * {
+ * "targetKeyValue": "key-value"
+ * }
+ *
+ *
+ * Response JSON:
+ *
+ * {
+ * "blockedModels": ["modelA", "modelB"]
+ * }
+ *
+ *
+ * @throws SQLException if database access fails
+ * @throws IOException if request or response I/O fails
+ */
+ private void getExclusive() throws SQLException, IOException {
+
+ String authHeader = clientRequest.getRequest().getHeader("Authorization");
+
+ // Validate Authorization header
+
+
+ String token = authHeader.substring("Bearer ".length()).trim();
+
+ // Get the key from the token
+ Key requester = DatabaseManager.getKeyByValue(token);
+
+ if ( !getAuthorization (authHeader, requester)){
+ return;
+ }
+
+ boolean isAdmin = isAdminRequest();
+ Key targetKey;
+ String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8);
+ JsonObject jsonObject = null;
+ if (!body.isEmpty()) {
+ jsonObject = JsonParser.parseString(body).getAsJsonObject();
+ }
+
+ try {
+ if(jsonObject.has("targetKeyValue")){
+ String targetKeyValue=jsonObject.get("targetKeyValue").getAsString();
+ if(!targetKeyValue.equals(token)&&requester.getRole()!=Role.Admin){
+ respond(ResponseFactory.MethodNotAllowed());
+ Logger.log("Unauthorized attempt to view another key's blocked models", LogLevel.warn);
+ Logger.log("Admin can view all, others cannot");
+ return;
+ }
+ //Check target key de se prepričam de obstaja
+ targetKey=DatabaseManager.getKeyByValue(targetKeyValue);
+ if (targetKey==null){
+ respond(ResponseFactory.BadRequest());
+ Logger.log("Target key not found: "+targetKeyValue,LogLevel.error);
+ return;
+ }
+ //Nucam ArrayList de lažje loopam skoz object pa tud za response generation
+ ArrayList+ * Behavior: + *
+ * {
+ * "targetKeyValue": "key-value",
+ * "modelName": "modelA,modelB,modelC"
+ * }
+ *
+ *
+ * Response JSON:
+ *
+ * {
+ * "allowedModels": { ...request body... }
+ * }
+ *
+ *
+ * @throws SQLException if database access fails
+ * @throws IOException if request or response I/O fails
+ */
+
+ private void addToExclusive() throws SQLException,IOException{
+
+ String authHeader = clientRequest.getRequest().getHeader("Authorization");
+
+
+ String token = authHeader.substring("Bearer ".length()).trim();
+
+ // Get the key from the token
+ Key requester = DatabaseManager.getKeyByValue(token);
+
+
+ if(!getAuthorization(authHeader,requester)){
+ return;
+ }
+ String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8);
+ JsonObject jsonObject = null;
+ if (!body.isEmpty()) {
+ jsonObject = JsonParser.parseString(body).getAsJsonObject();
+ }try {
+
+ if(jsonObject != null&&jsonObject.has("targetKeyValue")){
+ String targetKeyValue=jsonObject.get("targetKeyValue").getAsString();
+ //Key targetKey=DatabaseManager.getKeyByValue(targetKeyValue);
+ if(!jsonObject.has("modelName")&&jsonObject.get("modelName").isJsonNull()){
+ respond(ResponseFactory.BadRequest());
+ Logger.log("No models to add to table",LogLevel.error);
+ return;
+ }
+ String exclusiveModelStr=jsonObject.get("modelName").getAsString();
+ ArrayList+ * Behavior: + *
+ * {
+ * "targetKeyValue": "key-value",
+ * "modelName": "modelA,modelB"
+ * }
+ *
+ *
+ * Response JSON:
+ *
+ * {
+ * "UnblockedModels": { ...request body... }
+ * }
+ *
+ *
+ * @throws SQLException if database access fails
+ * @throws IOException if request or response I/O fails
+ */
+ private void deleteFromExclusive() throws SQLException,IOException{
+
+
+ String authHeader = clientRequest.getRequest().getHeader("Authorization");
+
+ String token = authHeader.substring("Bearer ".length()).trim();
+
+ Key requester = DatabaseManager.getKeyByValue(token);
+
+ if(!getAuthorization(authHeader,requester)){
+ return;
+ }
+
+ String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8);
+ JsonObject jsonObject = null;
+ if (!body.isEmpty()) {
+ jsonObject = JsonParser.parseString(body).getAsJsonObject();
+ }try {
+ if(jsonObject.has("targetKeyValue")){
+ String targetKeyValue=jsonObject.get("targetKeyValue").getAsString();
+ Key targetKey=DatabaseManager.getKeyByValue(String.valueOf(targetKeyValue));
+ if(jsonObject.has("modelName")&&jsonObject.get("modelName").isJsonNull()){
+ respond(ResponseFactory.BadRequest());
+ Logger.log("No models to delete from table",LogLevel.error);
+ return;
+ }
+ boolean allGood=false;
+ String exclusiveModelStr=jsonObject.get("modelName").getAsString();
+ ArrayList
+ * The requester must provide a valid {@code Authorization} header using the
+ * {@code Bearer
+ * By default, the requester can only retrieve information about their own key. + * If the requester has the {@link Role#Admin} role, they may optionally specify + * a different target key in the request body. + *
+ * + *+ * Optional JSON request body format: + *
+ * {
+ * "targetKeyValue": "some-key-value"
+ * }
+ *
+ *
+ *
+ * + * The response contains: + *
+ * Error responses: + *