diff --git a/.idea/misc.xml b/.idea/misc.xml
index 89c3cbb..f5d2b78 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
+
@@ -31,6 +52,12 @@
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 key is allowed to access a specific model. + * + *Permission rules (evaluated in order of precedence):
+ * + *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 + * + * @return {@code true} if access is permitted; {@code false} otherwise + * + * @throws SQLException if permission or exclusivity data cannot be retrieved + */ + public static synchronized boolean isAllowedForModel(String keyValue, String modelName ) throws SQLException { + //Vičič special + String exclusiveOwner = DatabaseManager.getExclusiveOwnerForModel(modelName); + if (exclusiveOwner != null && !exclusiveOwner.equals(keyValue)) { + Logger.warn("Model '" + modelName + "' is exclusive to another key."); + return false; + } + //Black list + if (DatabaseManager.getBlockedModelsForKey(keyValue).contains(modelName)) { + Logger.info("Model '" + modelName + "' is blocked for key " + keyValue); + return false; + } + + //White list + ArrayListIf 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; } @@ -74,6 +100,92 @@ public static synchronized void createKeysTable() throws SQLException { } } + /** + * Creates the {@code blocked_models} table in the database if it does not already exist. + * + *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 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" + + " FOREIGN KEY (key_value) REFERENCES keys(value) ON DELETE CASCADE\n" + + ");"; + + try (Connection conn = connect(); Statement statement = conn.createStatement()) { + statement.execute(sql); + Logger.info("Blocked models table created or already exists."); + } + } + + /** + * Creates the database table for storing allowed (whitelisted) models if it does not already exist. + * + *This method performs the following actions: + *
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"); + } + } + + public static synchronized void createExclusiveModelsTable() throws SQLException { + String sql = """ + CREATE TABLE IF NOT EXISTS model_exclusive ( + model_name TEXT PRIMARY KEY, + key_value TEXT NOT NULL, + FOREIGN KEY (key_value) REFERENCES keys(value) ON DELETE CASCADE + ); + """; + + try (Connection conn = connect(); + Statement stmt = conn.createStatement()) { + + stmt.execute(sql); + Logger.info("Model exclusivity table created or already exists."); + } + } + + + /** * Inserts a new {@link Key} into the {@code keys} table. * @@ -124,6 +236,36 @@ public static synchronized Key getKeyByValue(String value) throws SQLException { } } } + /** + * Retrieves a {@link Key} from the {@code keys} table based on its Name. + * + *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}.
+ * + * @param name the value of the key to be retrieved + * @return the {@link Key} object if found; {@code null} otherwise + * @throws SQLException if a database access error occurs or the SQL statement is invalid + */ + public static synchronized Key getKeyByName(String name) throws SQLException { + String sql = "SELECT * FROM keys WHERE name = ?"; + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, name); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + Key key = new Key( + rs.getInt("id"), + rs.getString("name"), + rs.getString("value"), + rs.getString("role") + ); + Logger.info("Key retrieved: " + key.getName()); + return key; + } else { + Logger.warn("No key found with name: " + name); + return null; + } + } + } /** * Retrieves all {@link Key} entries from the {@code keys} table. @@ -152,4 +294,867 @@ public static synchronized ArrayListThis 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 = ?"; + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, name); + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key deleted: " + name); + return true; + } else { + Logger.warn("No key found to delete with name: " + name); + return false; + } + } + } + + /** + * 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)) { + stmt.setString(1, value); + int affectedRows = stmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Key deleted: " + value); + return true; + } + + } + 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 = ?"; + + 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; + } + /** + * 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 = ?"; + + 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; + } + /** + * 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 changeKeyRoleByValue(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; + } + + /** + * 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 changeKeyNameByValue(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; + } + /** + * 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 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.
+ * + * @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 + String keyCheckSql = "SELECT value FROM keys WHERE value = ?"; + try (Connection conn = connect(); PreparedStatement checkStmt = conn.prepareStatement(keyCheckSql)) { + checkStmt.setString(1, keyValue); + ResultSet rs = checkStmt.executeQuery(); + + if (!rs.next()) { + Logger.warn("No key found with ID: " + keyValue); + throw new IllegalStateException("Key does not exist: " + keyValue); + } + } + + // Check if the model is blocked + 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; + + if (allowed) { + Logger.info("Key ID " + keyValue + " is allowed to use model '" + modelName + "'."); + } else { + Logger.info("Key ID " + keyValue + " is BLOCKED from using model '" + modelName + "'."); + } + + return allowed; + } + } + + /** + * Retrieves a list of models that the given key is allowed to use. + * + *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 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: + *
Authorization: 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/Overseer.java b/src/main/java/upr/famnit/managers/Overseer.java index 253f8d0..7fc48dd 100644 --- a/src/main/java/upr/famnit/managers/Overseer.java +++ b/src/main/java/upr/famnit/managers/Overseer.java @@ -330,7 +330,6 @@ public static Response sendCommand(WorkerCommand workerCommand, Socket senderReq if (workerRef == null) { return ResponseFactory.NotFound(); } - Request r = null; switch (workerCommand.command) { case "UPDATE" -> { diff --git a/src/main/java/upr/famnit/managers/connections/Client.java b/src/main/java/upr/famnit/managers/connections/Client.java index f1f30fd..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; @@ -9,6 +10,7 @@ import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; +import java.sql.SQLException; /** * The {@code ClientConnectionManager} class is responsible for handling incoming client connections. @@ -76,21 +78,25 @@ public void run() { Logger.error("Failed reading client request (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); return; } - - if (!RequestQue.addTask(cr)) { - Logger.error("Closing request due to invalid structure (" + clientSocket.getRemoteSocketAddress() + ")"); - Response failedResponse = ResponseFactory.MethodNotAllowed(); - try { - StreamUtil.sendResponse(cr.getClientSocket().getOutputStream(), failedResponse); - } catch (IOException e) { - Logger.error("Unable to respond to the client (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); - } - try { - cr.getClientSocket().close(); - } catch (IOException e) { - Logger.error("Unable to close connection to the client (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); + try { + if (!RequestQue.addTask(cr)) { + Logger.error("Closing request due to invalid structure (" + clientSocket.getRemoteSocketAddress() + ")"); + Response failedResponse = ResponseFactory.MethodNotAllowed(); + try { + StreamUtil.sendResponse(cr.getClientSocket().getOutputStream(), failedResponse); + } catch (IOException e) { + Logger.error("Unable to respond to the client (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); + } + try { + cr.getClientSocket().close(); + } catch (IOException e) { + Logger.error("Unable to close connection to the client (" + clientSocket.getRemoteSocketAddress() + "): " + e.getMessage()); + } + cr.getRequest().log(); } - cr.getRequest().log(); + } catch (SQLException e) { + Logger.log("Rejecting request because the database failed from:" +clientSocket.getRemoteSocketAddress()+ " "+e.getMessage() ); + rejectRequest(clientSocket); } } @@ -110,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 7b37ab6..bcc3a57 100644 --- a/src/main/java/upr/famnit/managers/connections/Management.java +++ b/src/main/java/upr/famnit/managers/connections/Management.java @@ -1,14 +1,15 @@ package upr.famnit.managers.connections; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import com.google.gson.*; import upr.famnit.authentication.*; 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; +import javax.xml.crypto.Data; import java.io.IOException; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -103,15 +104,419 @@ public void run() { case "/worker/versions" -> handleWorkerHiveVersionRoute(); case "/worker/command" -> handleWorkerCommandRoute(); case "/queue" -> handleQueueRoute(); + case "/block" -> handleWorkerBlockRoute(); + case "/allow"->handleWorkerWhiteList(); + case "/allow/list"-> handleAllowInfo(); + case "/exclusive"->handleExclusiveList(); case null, default -> respond(ResponseFactory.NotFound()); } } catch (IOException e) { Logger.error("Error handling proxy management request: " + e.getMessage()); + } catch (SQLException e) { + throw new RuntimeException(e); } Logger.success("Management request finished"); } + private void handleAllowInfo() throws SQLException, IOException { + switch (clientRequest.getRequest().getMethod()){ + case "GET" -> getInfo(); + } + } + + + /** + * Handles a request to retrieve information about an API key. + *
+ * 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: + *
+ * 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();
+ ArrayListThis 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(!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 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"); + + + 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 ( !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 + 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); + 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 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 + + 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 + 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(); case null, default -> respond(ResponseFactory.NotFound()); } } + /** + * 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); @@ -326,6 +1260,76 @@ private void handleInsertKeyRequest() throws IOException { respond(ResponseFactory.Ok(validKey.getValue().getBytes(StandardCharsets.UTF_8))); } + /** + * Handles DELETE requests to delete a authentication key from the system if you are not trying. + * to delete ADMIN + *This method performs the following actions: + *
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"); + 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 = JsonParser.parseString(body).getAsJsonObject(); + + boolean update = false; + + + 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; + try { + + + if (!auth.isEmpty() && name.isEmpty()) { + + if (bothChange) { + 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.changeKeyNameByValue(auth,newName); + respond(ResponseFactory.Ok(body.getBytes(StandardCharsets.UTF_8))); + return; + + } else if (hasNewRole) { + update = DatabaseManager.changeKeyRoleByValue(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)); + 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.changeKeyNameByName(name,newName); + 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. * @@ -396,4 +1507,108 @@ 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()
+ : "";
+ }
+
+ /**
+ * Retrieves all models that are blocked (blacklisted) for the specified key.
+ *
+ * This 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;
+ }
+
+
}
diff --git a/src/main/java/upr/famnit/managers/connections/Worker.java b/src/main/java/upr/famnit/managers/connections/Worker.java
index c27b8a9..dc743ec 100644
--- a/src/main/java/upr/famnit/managers/connections/Worker.java
+++ b/src/main/java/upr/famnit/managers/connections/Worker.java
@@ -357,6 +357,34 @@ private ClientRequest defaultPolling(Request request) {
return null;
}
+ /*
+ kinda threw out this shit
+ private ClientRequest defaultPolling(Request request) throws SQLException {
+ data.tagsTestAndSet(request.getUri());
+ String[] models = request.getUri().split(";");
+
+ for (String model : models) {
+ ClientRequest clientRequest = RequestQue.getTask(model, data.getNodeName());
+ if (clientRequest != null) {
+ String token = null;
+ Map