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 @@ - diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 251fc88..4703a31 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -10,10 +10,31 @@ + + - - - + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/main/java/upr/famnit/Main.java b/src/main/java/upr/famnit/Main.java index a9e06c3..bbf1e9c 100644 --- a/src/main/java/upr/famnit/Main.java +++ b/src/main/java/upr/famnit/Main.java @@ -7,6 +7,7 @@ import upr.famnit.util.Config; import upr.famnit.util.Logger; +import javax.xml.crypto.Data; import java.io.*; import java.sql.SQLException; @@ -16,6 +17,9 @@ public static void main(String[] args) { try { Config.init(); DatabaseManager.createKeysTable(); + DatabaseManager.createBlockedModelsTable(); + DatabaseManager.createWhiteListTable(); + DatabaseManager.createExclusiveModelsTable(); WorkerServer workerServer = new WorkerServer(); ClientServer clientServer = new ClientServer(); diff --git a/src/main/java/upr/famnit/authentication/AllowedModelsTable.java b/src/main/java/upr/famnit/authentication/AllowedModelsTable.java new file mode 100644 index 0000000..0b722d4 --- /dev/null +++ b/src/main/java/upr/famnit/authentication/AllowedModelsTable.java @@ -0,0 +1,23 @@ +package upr.famnit.authentication; + +import upr.famnit.components.NodeData; +import upr.famnit.components.WorkerVersion; +import upr.famnit.managers.connections.Worker; + +import java.util.ArrayList; +import java.util.List; + +public class AllowedModelsTable { + private Key key; + private ArrayListtagsAllowed; + + public void SetKey(Key kateri){ + this.key=kateri; + } + public Key getKey(Key kateri){ + return key; + } + public void getArr(){ + + } +} diff --git a/src/main/java/upr/famnit/authentication/Role.java b/src/main/java/upr/famnit/authentication/Role.java index 7e4ce69..e633ca8 100644 --- a/src/main/java/upr/famnit/authentication/Role.java +++ b/src/main/java/upr/famnit/authentication/Role.java @@ -10,6 +10,7 @@ *
  • {@code Client}: Represents a standard client with limited access rights.
  • *
  • {@code Worker}: Represents a worker entity with elevated permissions.
  • *
  • {@code Admin}: Represents an administrator with full access rights.
  • + *
  • {@code Researcher}: Represents an administrator with full access rights.
  • *
  • {@code Unknown}: Represents an undefined or unrecognized role.
  • * *

    @@ -35,6 +36,11 @@ public enum Role { */ Admin, + /** + * Represents a researcher with lower priority queing + */ + Researcher, + /** * Represents an undefined or unrecognized role. */ @@ -60,9 +66,21 @@ public String toString() { case Admin -> { return "Admin"; } + case Researcher ->{ + return "Researcher"; + } default -> { return "Unknown"; } } } + public static Role fromString(String a) { + if (a == null) return null; + try { + return Role.valueOf(a.trim()); + } catch (IllegalArgumentException e) { + return null; // unknown string + } + } + } diff --git a/src/main/java/upr/famnit/authentication/RoleUtil.java b/src/main/java/upr/famnit/authentication/RoleUtil.java index be1a072..ee1bec8 100644 --- a/src/main/java/upr/famnit/authentication/RoleUtil.java +++ b/src/main/java/upr/famnit/authentication/RoleUtil.java @@ -47,6 +47,9 @@ public static Role fromString(String role) { case "admin" -> { return Role.Admin; } + case "researcher"->{ + return Role.Researcher; + } default -> { return Role.Unknown; } diff --git a/src/main/java/upr/famnit/components/Request.java b/src/main/java/upr/famnit/components/Request.java index 89c53f7..148537f 100644 --- a/src/main/java/upr/famnit/components/Request.java +++ b/src/main/java/upr/famnit/components/Request.java @@ -204,6 +204,17 @@ public byte[] getBody() { return body != null ? body.clone() : null; } + /** + * Retrieves the head of the request. + * + * @return the request head as a String, or {@code null} if no head is present + */ + public String getHeader(String name) { + if (name == null) return null; + return headers.get(name.toLowerCase()); + } + + /** * Logs the details of the request using the {@link Logger}. * diff --git a/src/main/java/upr/famnit/components/RequestQue.java b/src/main/java/upr/famnit/components/RequestQue.java index bf134b6..4e3428b 100644 --- a/src/main/java/upr/famnit/components/RequestQue.java +++ b/src/main/java/upr/famnit/components/RequestQue.java @@ -1,10 +1,14 @@ package upr.famnit.components; +import upr.famnit.managers.DatabaseManager; +import upr.famnit.util.LogLevel; import upr.famnit.util.Logger; import upr.famnit.util.StreamUtil; +import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -94,11 +98,16 @@ public static ClientRequest getModelTask(String modelName, String nodeName) { * @param request the {@link ClientRequest} to be added * @return {@code true} if the task was successfully added; {@code false} otherwise */ - public static boolean addTask(ClientRequest request) { + public static boolean addTask(ClientRequest request) throws SQLException { if (request.getRequest().getProtocol().equals("HIVE")) { return false; } - + String model = extractModel(request); + String token = extractToken(request); + if(!isAllowedForModel(model,token)){ + Logger.log("Model not allowed for the person", LogLevel.error); + return false; + } if (request.getRequest().getHeaders().containsKey("node")) { return addToQueByNode(request); } else { @@ -236,5 +245,89 @@ public static ClientRequest getUnhandlableTask(ArrayList nodeNames, Set< return null; } + /** + * Extracts the model name from the JSON request body. + * + *

    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):

    + * + *
      + *
    1. Global exclusivity (strongest rule):
      + * If the model is exclusive to a different key, access is denied. + * If the model is not exclusive or is exclusive to this key, evaluation continues.
    2. + * + *
    3. Block list (hard deny):
      + * If the model appears in the key's block list, access is denied.
    4. + * + *
    5. Allow list (conditional whitelist):
      + * If the allow list is non-empty, the model must be present in it. + * If the allow list is empty, all non-blocked models are permitted.
    6. + *
    + * + *

    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 + ArrayList allow = DatabaseManager.getAllowedModelsForKey(keyValue); + if (!allow.isEmpty() && !allow.contains(modelName)) { + Logger.info("Model '" + modelName + "' not in allow list for key " + keyValue); + return false; + } + + return true; + } } diff --git a/src/main/java/upr/famnit/managers/DatabaseManager.java b/src/main/java/upr/famnit/managers/DatabaseManager.java index d92eb6e..6ec370c 100644 --- a/src/main/java/upr/famnit/managers/DatabaseManager.java +++ b/src/main/java/upr/famnit/managers/DatabaseManager.java @@ -1,6 +1,7 @@ package upr.famnit.managers; import upr.famnit.authentication.Key; +import upr.famnit.authentication.Role; import upr.famnit.util.Logger; import java.sql.*; @@ -37,17 +38,42 @@ public class DatabaseManager { /** * Establishes a connection to the SQLite database using the configured database URL. * - *

    If 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: + *

      + *
    • Concurrent reads and writes without blocking each other.
    • + *
    • Faster commits because changes are written to a log instead of directly overwriting the main database.
    • + *
    • Reduced chances of hitting SQLITE_BUSY errors when multiple threads access the database.
    • + *
    + *

    + * + *

    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: + *

      + *
    1. Defines a SQL statement to create the allowed_models table with columns: + *
        + *
      • key_value (TEXT, NOT NULL)
      • + *
      • model_name (TEXT, NOT NULL)
      • + *
      + *
    2. + *
    3. Sets a composite primary key on (key_value, model_name).
    4. + *
    5. Creates a foreign key constraint linking key_value to the keys table, + * with cascading delete.
    6. + *
    7. Executes the SQL statement using a new database connection.
    8. + *
    9. Logs a message indicating that the table was successfully created.
    10. + *
    + *

    + * + *

    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 ArrayList getAllKeys() throws SQLException { } return keys; } + + /** + * Deletes a key from the {@code keys} table based on its name. + * + *

    This 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: + *

      + *
    • Warnings are logged if the key does not exist or is an admin
    • + *
    • Info is logged when a model is successfully blocked for a key
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to block for this key + * + * @throws SQLException if a database access error occurs or the SQL statement is invalid + * @throws IllegalStateException if attempting to block a model for an admin key + */ + + public static synchronized void blockModelForKey(String keyValue, String modelName) throws SQLException { + // First: check the user's role + String roleSql = "SELECT role FROM keys WHERE value = ?"; + try (Connection conn = connect(); PreparedStatement roleStmt = conn.prepareStatement(roleSql)) { + roleStmt.setString(1, keyValue); + ResultSet rs = roleStmt.executeQuery(); + + if (rs.next()) { + String role = rs.getString("role"); + + // Prevent blocking for admin users + if ("admin".equalsIgnoreCase(role)) { + Logger.warn("Attempted to block a model for admin key ID: " + keyValue); + throw new IllegalStateException("Admin keys cannot have models blocked."); + } + } else { + Logger.warn("No key found with value: " + keyValue); + throw new IllegalStateException("Key does not exist: " + keyValue); + } + } + + // Insert into blocked_models table + String insertSql = "INSERT INTO blocked_models (key_value, model_name) VALUES (?, ?)"; + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(insertSql)) { + stmt.setString(1, keyValue); + stmt.setString(2, modelName); + stmt.executeUpdate(); + Logger.info("Blocked model '" + modelName + "' for key ID " + keyValue); + } + catch (SQLException e){ + e.printStackTrace(); + Logger.log("Something went wrong"); + } + } + + /** + * Removes a blocked model entry for the given key in the {@code blocked_models} table. + * + *

    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: + *

      + *
    • Warnings are logged if the key does not exist or the model was not blocked
    • + *
    • Info is logged when a model is successfully unblocked for a key
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to unblock for this key + * + * @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 void unblockModelForKey(String keyValue, String modelName) throws SQLException { + // Check if 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); + } + } + + // Delete the blocked model + String deleteSql = "DELETE FROM blocked_models WHERE key_value = ? AND model_name = ?"; + try (Connection conn = connect(); PreparedStatement deleteStmt = conn.prepareStatement(deleteSql)) { + deleteStmt.setString(1, keyValue); + deleteStmt.setString(2, modelName); + + int affectedRows = deleteStmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Unblocked model '" + modelName + "' for key ID " + keyValue); + } else { + Logger.warn("Model '" + modelName + "' was not blocked for key ID " + keyValue); + } + } + } + /** + * 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: + *

      + *
    • Info is logged whether the key is allowed or blocked for the model
    • + *
    • Warnings are logged if the key does not exist
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to check + * + * @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 + */ + 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)) { + 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 blocked_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; + } + } + + /** + * Determines whether a key is permitted to use a given model by consulting the database. + * + *

    This method enforces the following policy:

    + *
      + *
    • First, the key must exist in the {@code keys} table; if it does not, an + * {@link IllegalStateException} is thrown.
    • + *
    • Then the method queries {@code allowed_models} for a row matching + * {@code (key_value, model_name)}.
    • + *
    • If such a row exists, the key is treated as blocked for that model.
    • + *
    • If no matching row exists, the key is treated as allowed for that model.
    • + *
    + * + *

    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: + *

      + *
    • Warnings if the key does not exist
    • + *
    • Info showing how many models are allowed
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @return a {@link ArrayList} of model names that the key is allowed to use + * @throws SQLException if a database access error occurs + * @throws IllegalStateException if the key does not exist + */ + public static synchronized ArrayList getBlockedModelsForKey(String keyValue) 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); + } + } + + String sql = "SELECT model_name FROM blocked_models WHERE key_value = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, keyValue); + ResultSet rs = stmt.executeQuery(); + + ArrayList allowedModels = new ArrayList<>(); + while (rs.next()) { + allowedModels.add(rs.getString("model_name")); + } + + Logger.info("Key ID " + keyValue + " is allowed to use " + allowedModels.size() + " models."); + return allowedModels; + } + } + + /** + * Blocks access to a specific model for the given key in the {@code allowed_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 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: + *

      + *
    • Warnings are logged if the key does not exist or is an admin
    • + *
    • Info is logged when a model is successfully blocked for a key
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to block for this key + * + * @throws SQLException if a database access error occurs or the SQL statement is invalid + * @throws IllegalStateException if attempting to block a model for an admin key + */ + + public static synchronized void allowModelForKey(String keyValue, String modelName) throws SQLException { + // First: check the user's role + String roleSql = "SELECT role FROM keys WHERE value = ?"; + try (Connection conn = connect(); PreparedStatement roleStmt = conn.prepareStatement(roleSql)) { + roleStmt.setString(1, keyValue); + ResultSet rs = roleStmt.executeQuery(); + + if (rs.next()) { + String role = rs.getString("role"); + + // Prevent blocking for admin users + if ("admin".equalsIgnoreCase(role)) { + Logger.warn("Attempted to block a model for admin key ID: " + keyValue); + throw new IllegalStateException("Admin keys cannot have models blocked."); + } + } else { + Logger.warn("No key found with value: " + keyValue); + throw new IllegalStateException("Key does not exist: " + keyValue); + } + } + + // Insert into allowed_models table + String insertSql = "INSERT INTO allowed_models (key_value, model_name) VALUES (?, ?)"; + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(insertSql)) { + stmt.setString(1, keyValue); + stmt.setString(2, modelName); + stmt.executeUpdate(); + Logger.info("Allowed model: '" + modelName + "' for key ID " + keyValue); + } + catch (SQLException e){ + e.printStackTrace(); + Logger.log("Something went wrong"); + } + } + + + /** + * Removes an allowed model entry for the given key in the {@code allowed_models} table. + * + *

    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: + *

      + *
    • Warnings are logged if the key does not exist or the model was not blocked
    • + *
    • Info is logged when a model is successfully unblocked for a key
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to unblock for this key + * + * @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 void deleteFromWhiteList(String keyValue, String modelName) throws SQLException { + // Check if 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); + } + } + + // Delete the blocked model + String deleteSql = "DELETE FROM allowed_models WHERE key_value = ? AND model_name = ?"; + try (Connection conn = connect(); PreparedStatement deleteStmt = conn.prepareStatement(deleteSql)) { + deleteStmt.setString(1, keyValue); + deleteStmt.setString(2, modelName); + + int affectedRows = deleteStmt.executeUpdate(); + if (affectedRows > 0) { + Logger.info("Unblocked model '" + modelName + "' for key ID " + keyValue); + } else { + Logger.warn("Model '" + modelName + "' was not blocked for key ID " + keyValue); + } + } + } + /** + * Checks whether a given key is allowed to use a specific model. + * + *

    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: + *

      + *
    • Info is logged whether the key is allowed or blocked for the model
    • + *
    • Warnings are logged if the key does not exist
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @param modelName the name of the model to check + * + * @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 + */ + public static synchronized boolean canKeyUseModelWhite(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: + *

      + *
    • Warnings if the key does not exist
    • + *
    • Info showing how many models are allowed
    • + *

    + * + * @param keyValue the unique ID of the key in the {@code keys} table + * @return a {@link ArrayList} of model names that the key is allowed to use + * @throws SQLException if a database access error occurs + * @throws IllegalStateException if the key does not exist + */ + public static synchronized ArrayList getAllowedModelsForKey(String keyValue) 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); + } + } + + String sql = "SELECT model_name FROM allowed_models WHERE key_value = ?"; + + try (Connection conn = connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, keyValue); + ResultSet rs = stmt.executeQuery(); + + ArrayList allowedModels = new ArrayList<>(); + while (rs.next()) { + allowedModels.add(rs.getString("model_name")); + } + + Logger.info("Key ID " + keyValue + " is allowed to use " + allowedModels.size() + " models."); + return allowedModels; + } + } + + /** + * Assigns exclusive access of a model to a target key. + * + *

    Authorization: Only keys with the {@code admin} role may call this method.

    + * + *

    Behavior: + *

      + *
    • If the model is not currently exclusive, it becomes exclusive to {@code targetKey}.
    • + *
    • If the model is already exclusive, ownership is reassigned to {@code targetKey}.
    • + *
    • At most one key may own a model at any time.
    • + *
    + * + *

    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: + *

      + *
    • The model is exclusive and owned by the key, OR
    • + *
    • The model is not exclusive at all
    • + *
    + * + * @param keyValue the key attempting to use the model + * @param modelName the model being checked + * + * @return {@code true} if the model is either not exclusive + * or exclusive to {@code keyValue}; + * {@code false} if the model is exclusive to another key + * + * @throws IllegalStateException if the key does not exist + * @throws SQLException if a database error occurs + */ + public static synchronized boolean canKeyUseExclusive(String keyValue, String modelName) throws SQLException { + + // Verify key exists + String keyCheckSql = "SELECT 1 FROM keys WHERE value = ?"; + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(keyCheckSql)) { + + stmt.setString(1, keyValue); + ResultSet rs = stmt.executeQuery(); + + if (!rs.next()) { + throw new IllegalStateException("Key does not exist: " + keyValue); + } + } + + // Check exclusivity ownership + 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(); + + // Not exclusive → usable + if (!rs.next()) { + return true; + } + + // Exclusive → must match owner + return keyValue.equals(rs.getString("key_value")); + } + } + + /** + * Retrieves all models that are exclusively assigned to a given key. + * + *

    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 ArrayList getExclusiveModelsForKey(String keyValue) + throws SQLException { + + String sql = "SELECT model_name FROM model_exclusive WHERE key_value = ?"; + + ArrayList models = new ArrayList<>(); + + try (Connection conn = connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, keyValue); + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + models.add(rs.getString("model_name")); + } + } + + return models; + } + + /** + * Retrieves the key that owns exclusive access to a given model. + * + *

    If 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 } scheme. The token is used to identify the requesting key. + *

    + * + *

    + * 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: + *

      + *
    • The target key name
    • + *
    • A list of allowed models
    • + *
    • A list of exclusive models
    • + *
    + *

    + * + *

    + * Error responses: + *

      + *
    • {@code 400 Bad Request} – missing/invalid authorization header or target key not found
    • + *
    • {@code 405 Method Not Allowed} – invalid token or insufficient permissions
    • + *
    + *

    + * + * @throws IOException if an I/O error occurs while reading the request or sending the response + * @throws SQLException if a database access error occurs while retrieving key or model data + */ + + private void getInfo() 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(); + Key requester = DatabaseManager.getKeyByValue(token); + + if (requester == null) { + respond(ResponseFactory.MethodNotAllowed()); + return; + } + + // Read optional body + String body = new String(clientRequest.getRequest().getBody(), StandardCharsets.UTF_8); + JsonObject jsonObject = body.isEmpty() ? null : JsonParser.parseString(body).getAsJsonObject(); + + Key targetKey = requester; + + // Optional: admin can query another key + if (jsonObject != null && jsonObject.has("targetKeyValue")) { + String targetKeyValue = jsonObject.get("targetKeyValue").getAsString(); + + // Non-admins can only query themselves + if (!targetKeyValue.equals(token) && requester.getRole() != Role.Admin) { + respond(ResponseFactory.MethodNotAllowed()); + return; + } + + targetKey = DatabaseManager.getKeyByValue(targetKeyValue); + if (targetKey == null) { + respond(ResponseFactory.BadRequest()); + return; + } + } + String val = targetKey.getValue(); + + + ArrayList allowedModels = + DatabaseManager.getAllowedModelsForKey(val); + + JsonArray modelsArray = new JsonArray(); + for (String model : allowedModels) { + modelsArray.add(model); + } + ArrayList exclusiveModels= + DatabaseManager.getExclusiveModelsForKey(val); + JsonArray exclusiveModel = new JsonArray(); + for(String model: exclusiveModels){ + exclusiveModel.add(model); + } + + JsonObject response = new JsonObject(); + response.addProperty("keyName", targetKey.getName()); + response.add("allowedModels", modelsArray); + response.add("Exclusive models", exclusiveModel); + + respond(ResponseFactory.Ok( + response.toString().getBytes(StandardCharsets.UTF_8) + )); + + Logger.log( + "Returned " + allowedModels.size() + " allowed models for key " + targetKey.getName(), + LogLevel.info + ); + } + + + + /** + * Routes requests for the exclusive list endpoint based on HTTP method. + *

    + * Supported methods: + *

      + *
    • GET – Retrieves exclusive (blocked) models
    • + *
    • POST – Adds models to the exclusive list
    • + *
    • DELETE – Removes models from the exclusive list
    • + *
    + * + * @throws SQLException if a database access error occurs + * @throws IOException if request or response I/O fails + */ + + private void handleExclusiveList() throws SQLException, IOException { + switch (clientRequest.getRequest().getMethod()){ + case "GET" -> getExclusive(); + case "POST"->addToExclusive(); + case "DELETE"->deleteFromExclusive(); + } + } + + + + /** + * Handles GET requests for retrieving exclusive (blocked) models for a key. + *

    + * Behavior: + *

      + *
    • Extracts and validates the Authorization bearer token
    • + *
    • Determines the requesting key and checks authorization
    • + *
    • Allows admins to view blocked models for any key
    • + *
    • Restricts non-admin users to viewing only their own blocked models
    • + *
    • Returns a JSON array of blocked model names
    • + *
    + * + * Expected JSON body (optional): + *
    +     * {
    +     *   "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 + ArrayListexclusiveModels=getExclusiveModels(targetKeyValue); + JsonArray responseArray=new JsonArray(); + for(String model:exclusiveModels){ + responseArray.add(model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("blockedModels",responseArray); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + Logger.log("Retrieved: "+exclusiveModels.size() + " blocked models for key " + requester.getName(), LogLevel.info); + } + }catch (SQLException e){ + e.printStackTrace(); + respond(ResponseFactory.InternalServerError()); + } + + } + + + /** + * Handles POST requests for adding models to a key's exclusive list. + *

    + * Behavior: + *

      + *
    • Extracts and validates the Authorization bearer token
    • + *
    • Checks requester authorization
    • + *
    • Parses model names from the request body
    • + *
    • Adds one or more models as exclusive for the target key
    • + *
    + * + * Expected JSON body: + *
    +     * {
    +     *   "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 targetModelList = new ArrayList<>(Arrays.asList(exclusiveModelStr.split(","))); + for (String model:targetModelList){ + DatabaseManager.setExclusiveModelForKey(requester.getValue(),targetKeyValue,model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("allowedModels",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + respond(ResponseFactory.Ok()); + } catch (SQLException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + + } + + + /** + * Handles DELETE requests for removing models from a key's exclusive list. + *

    + * Behavior: + *

      + *
    • Extracts and validates the Authorization bearer token
    • + *
    • Checks requester authorization
    • + *
    • Verifies whether each model is currently exclusive
    • + *
    • Removes exclusive restrictions for valid models
    • + *
    • Skips models that are already allowed
    • + *
    + * + * Expected JSON body: + *
    +     * {
    +     *   "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 targetModelList = new ArrayList<>(Arrays.asList(exclusiveModelStr.split(","))); + for (String model:targetModelList){ + boolean exclusive=DatabaseManager.canKeyUseExclusive(targetKeyValue,model); + if(exclusive){ + DatabaseManager.removeExclusiveModel(requester.getValue(),model); + allGood=true; + continue; + } + Logger.log("Skipping this model, Model is already allowed",LogLevel.info); + } + if(!allGood){ + respond(ResponseFactory.BadRequest()); + Logger.log("Something went wrong",LogLevel.error); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("UnblockedModels",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + + } catch (SQLException | IOException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + + } /** @@ -135,10 +540,12 @@ private void handleQueueRoute() throws IOException { * * @throws IOException if an I/O error occurs during response transmission */ - private void handleKeyRoute() throws IOException { + private void handleKeyRoute() throws IOException, SQLException { switch (clientRequest.getRequest().getMethod()) { case "GET" -> handleListKeysRequest(); case "POST" -> handleInsertKeyRequest(); + case "DELETE" -> handleDeleteKeyRequest(); + case "PATCH" -> handleKeyChangeReq(); case null, default -> respond(ResponseFactory.NotFound()); } } @@ -191,6 +598,495 @@ private void handleWorkerTagsRoute() throws IOException { } } + /** + * Routes incoming requests related to blocked models (blacklist) to the appropriate handler + * based on the HTTP method. + * + *

    This method performs the following actions: + *

      + *
    1. If the method is GET, it retrieves all blocked models via {@link #getAllBlock()}.
    2. + *
    3. If the method is POST, it inserts new blocked models via {@link #insertModelBlock()}.
    4. + *
    5. If the method is DELETE, it removes blocked models via {@link #deleteModelBlock()}.
    6. + *
    + *

    + * + * @throws IOException If an error occurs while sending the response. + * @throws SQLException If a database access error occurs while handling the request. + */ + + private void handleWorkerBlockRoute() throws IOException, SQLException { + switch (clientRequest.getRequest().getMethod()){ + case "GET" -> getAllBlock(); + case "POST"-> insertModelBlock(); + case "DELETE"-> deleteModelBlock(); + } + } + + /** + * Routes incoming requests related to allowed models (whitelist) to the appropriate handler + * based on the HTTP method. + * + *

    This method performs the following actions: + *

      + *
    1. If the method is GET, it retrieves all allowed models via {@link #getAllAllowed()}.
    2. + *
    3. If the method is POST, it inserts new allowed models via {@link #insertAllowance()}.
    4. + *
    5. If the method is DELETE, it removes allowed models via {@link #deleteAllowedModel()}.
    6. + *
    + *

    + * + * @throws IOException If an error occurs while sending the response. + * @throws SQLException If a database access error occurs while handling the request. + */ + + private void handleWorkerWhiteList() throws IOException,SQLException{ + switch (clientRequest.getRequest().getMethod()){ + case "GET" -> getAllAllowed(); + case "POST" -> insertAllowance(); + case "DELETE" -> deleteAllowedModel(); + } + } + /** + * Handles the deletion of one or more allowed models from a target key's whitelist. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the incoming JSON body to obtain the target key and the models to remove.
    4. + *
    5. Validates that the required fields (`targetKeyValue` and `modelName`) are present and correctly formatted.
    6. + *
    7. Iterates over each provided model and removes it from the target key's whitelist if it is currently disallowed.
    8. + *
    9. Logs the status of each removal attempt, including skipped models that are already allowed.
    10. + *
    11. Sends an appropriate HTTP response indicating success or failure, including JSON details of processed models.
    12. + *
    + *

    + * + *

    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(); + ArrayList targetModelList = new ArrayList<>(Arrays.asList(allowedModelStr.split(","))); + for (String model:targetModelList){ + boolean allowed=DatabaseManager.canKeyUseModelWhiteDataBase(targetKeyValue,model); + if(!allowed){ + DatabaseManager.deleteFromWhiteList(targetKeyValue,model); + allGood=true; + continue; + } + Logger.log("Skipping this model, Model is already allowed",LogLevel.info); + } + if(!allGood){ + respond(ResponseFactory.BadRequest()); + Logger.log("Something went wrong",LogLevel.error); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("Not allowed models: ",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + + } catch (SQLException | IOException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + + + } + + /** + * Adds one or more models to a target key's whitelist, granting that key permission to use them. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the request body as JSON to retrieve the target key and list of models to allow.
    4. + *
    5. Validates that the required fields (`targetKeyValue` and `modelName`) are present and non-null.
    6. + *
    7. Splits the comma-separated list of models and inserts each into the target key's whitelist.
    8. + *
    9. Constructs a JSON response confirming the allowed models and returns it to the client.
    10. + *
    11. Logs errors and stops execution if invalid data or database issues occur.
    12. + *
    + *

    + * + *

    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(); + ArrayList targetModelList = new ArrayList<>(Arrays.asList(allowedModelStr.split(","))); + for (String model:targetModelList){ + DatabaseManager.allowModelForKey(targetKeyValue,model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("allowedModels",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + respond(ResponseFactory.Ok()); + } catch (SQLException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + + } + + /** + * Retrieves all models allowed for a specified key, returning them as a JSON array. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the incoming JSON body to obtain the targetKeyValue.
    4. + *
    5. Ensures that only administrators—or the key owner—may view another key's allowed models.
    6. + *
    7. Verifies that the specified target key exists in the database.
    8. + *
    9. Fetches all models allowed for the target key and converts them into a JSON response.
    10. + *
    11. Sends a successful response containing the list of allowed models.
    12. + *
    13. Logs important actions, including unauthorized attempts or retrieval statistics.
    14. + *
    + *

    + * + *

    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 + ArrayListallowedModels=getAllowedWhite(targetKeyValue); + JsonArray responseArray=new JsonArray(); + for(String model:allowedModels){ + responseArray.add(model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("blockedModels",responseArray); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + Logger.log("Retrieved: "+allowedModels.size() + " blocked models for key " + requester.getName(), LogLevel.info); + } + }catch (SQLException e){ + e.printStackTrace(); + respond(ResponseFactory.InternalServerError()); + } + + } + + /** + * Removes one or more blocked models from a target key's blacklist, effectively unblocking them. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the incoming JSON body to retrieve the targetKeyValue and models to unblock.
    4. + *
    5. Validates that the modelName field exists and is not null.
    6. + *
    7. Iterates through the provided models and unblocks any that are currently blocked for the target key.
    8. + *
    9. Logs actions, including skipped models that are not currently blocked.
    10. + *
    11. Constructs a JSON response confirming the unblocked models and sends it back to the client.
    12. + *
    + *

    + * + *

    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.

    + * + * @throws IOException If an error occurs while reading the request body or sending a response. + * @throws SQLException If a database access error occurs while checking or unblocking models. + */ + private void deleteModelBlock() 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(); + ArrayList targetModelList = new ArrayList<>(Arrays.asList(allowedModelStr.split(","))); + for (String model:targetModelList){ + boolean allowed=DatabaseManager.canKeyUseModelBlack(targetKeyValue,model); + if(!allowed){ + DatabaseManager.unblockModelForKey(targetKeyValue,model); + allGood=true; + continue; + } + Logger.log("Skipping this model, Model is already allowed",LogLevel.info); + } + if(!allGood){ + respond(ResponseFactory.BadRequest()); + Logger.log("Something went wrong",LogLevel.error); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("UnblockedModels",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + + } catch (SQLException | IOException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + } + + /** + * Blocks one or more models for a specified key, preventing that key from using them. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the JSON request body to obtain the target key and list of models to block.
    4. + *
    5. Ensures that the required fields (targetKeyValue and modelName) are present and non-null.
    6. + *
    7. Splits the comma-separated list of model names and adds each to the target key's block list.
    8. + *
    9. Constructs a JSON response confirming the blocked models and sends it back to the client.
    10. + *
    11. Logs errors when invalid data is provided or if a database issue occurs.
    12. + *
    + *

    + * + *

    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(); + ArrayList targetModelList = new ArrayList<>(Arrays.asList(allowedModelStr.split(","))); + for (String model:targetModelList){ + DatabaseManager.blockModelForKey(targetKeyValue,model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("allowedModels",jsonObject); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + } + respond(ResponseFactory.Ok()); + } catch (SQLException e) { + e.printStackTrace(); + Logger.log("There was something wrong with getting the data from the database"); + } + + } + + /** + * Retrieves all models blocked for a specified key and returns them as a JSON array. + * + *

    This method performs the following actions: + *

      + *
    1. Extracts and validates the Authorization header to authenticate the requester.
    2. + *
    3. Parses the JSON request body to obtain the targetKeyValue.
    4. + *
    5. Ensures that only administrators—or the key owner—may view another key's blocked models.
    6. + *
    7. Verifies that the specified target key exists in the database.
    8. + *
    9. Fetches all models blocked for the target key and converts them into a JSON response.
    10. + *
    11. Sends a success response containing the list of blocked models.
    12. + *
    13. Logs key events, including unauthorized access attempts and retrieval statistics.
    14. + *
    + *

    + * + *

    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 + ArrayListallowedModels=getAllowedBlack(targetKeyValue); + JsonArray responseArray=new JsonArray(); + for(String model:allowedModels){ + responseArray.add(model); + } + JsonObject responseJson=new JsonObject(); + responseJson.add("blockedModels",responseArray); + byte[] responseBytes=responseJson.toString().getBytes(StandardCharsets.UTF_8); + respond(ResponseFactory.Ok(responseBytes)); + Logger.log("Retrieved: "+allowedModels.size() + " blocked models for key " + requester.getName(), LogLevel.info); + } + }catch (SQLException e){ + e.printStackTrace(); + respond(ResponseFactory.InternalServerError()); + } + } + /** * Handles requests to the "/worker/version/hive" route, providing the Hive versions of active workers. * @@ -203,13 +1099,51 @@ private void handleWorkerHiveVersionRoute() throws IOException { } } - private void handleWorkerCommandRoute() throws IOException { + /** + * Routes incoming worker-related requests to the appropriate handler based on the HTTP method. + * + *

    This method performs the following actions: + *

      + *
    1. Reads the HTTP method from the incoming {@link ClientRequest}.
    2. + *
    3. If the method is POST, it delegates processing to + * {@link #handleWorkersCommandRequest()}.
    4. + *
    5. For unsupported or missing methods, it responds with a 404 Not Found status.
    6. + *
    + *

    + * + *

    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: + *

      + *
    1. Parses the JSON request body into a {@link WorkerCommand} object.
    2. + *
    3. Logs the worker identifier and the command received for debugging and audit purposes.
    4. + *
    5. Delegates the command to {@link Overseer#sendCommand(WorkerCommand, java.net.Socket)} + * for processing.
    6. + *
    7. If the Overseer returns a response, it is immediately sent back to the client.
    8. + *
    + *

    + * + *

    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: + *

      + *
    1. Parses the incoming JSON body to create a {@link SubmittedKey} object.
    2. + *
    3. Converts the submitted key into a {@link Key} object.
    4. + *
    5. Deletes the whole record with that key {@link DatabaseManager}.
    6. + *
    7. Sends a successful response containing the key's value.
    8. + *
    + *

    + * + * @throws IOException if an I/O error occurs during request processing or response transmission + */ + private void handleDeleteKeyRequest() throws IOException, SQLException { + Gson gson = new GsonBuilder().create(); + 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); + 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; + boolean deleteByValue = false; + + + try { + if (!value.isEmpty()) { + key = DatabaseManager.getKeyByValue(value); + deleteByValue = true; + } + if (key == null && !name.isEmpty()) { + key = DatabaseManager.getKeyByName(name); + } + + if (key != null) { + 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 (key.getRole() != Role.Admin) { + // Key exists and is NOT Admin → delete it + Logger.log(key.toString()); + boolean deleted = DatabaseManager.deleteKeyByName(key.getName()); + + if (deleted) { + Logger.log("Key deleted: " + key.getName(), LogLevel.success); + respond(ResponseFactory.Ok(name.getBytes())); + return; + + } + } + } + respond(ResponseFactory.NotFound()); + Logger.log("Key could not be deleted", LogLevel.error); + } catch (SQLException e) { + e.printStackTrace(); + //respond(ResponseFactory.BadRequest()); + } + } + + /** * Handles GET requests to list all authentication keys in the system. * @@ -359,6 +1363,113 @@ 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: + *

      + *
    • role — via {@code roleNew}
    • + *
    • name — via {@code newName}
    • + *
    • or both
    • + *
    + * + *

    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:

    + *
      + *
    • Identify key by authentication value ({@code value})
    • + *
    • Identify key by name ({@code name})
    • + *
    • Optionally update role, name, or both
    • + *
    + * + *

    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.

    + * + * @param obj The JSON object to read from. + * @param key The name of the field whose string value should be retrieved. + * + * @return The trimmed string value associated with the key, or an empty string if + * the key is missing or its value is 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 ArrayList getAllowedBlack(String keyValue) throws SQLException { + return DatabaseManager.getBlockedModelsForKey(keyValue); + } + + /** + * Retrieves all models that are allowed (whitelisted) for the specified key. + * + *

    This 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 ArrayList getAllowedWhite(String keyValue) throws SQLException { + return DatabaseManager.getAllowedModelsForKey(keyValue); + } + + private ArrayList getExclusiveModels(String keyValue) throws SQLException { + return DatabaseManager.getExclusiveModelsForKey(keyValue); + } + + /** + * Validates the authorization header and ensures that the requester has administrative privileges. + * + *

    This method performs the following actions: + *

      + *
    1. Validates that the Authorization header exists and begins with the expected "Bearer" format.
    2. + *
    3. Ensures that the requester key is valid and corresponds to a real API key.
    4. + *
    5. Checks whether the requester is an administrator, as only admins may perform certain actions.
    6. + *
    7. Sends the appropriate HTTP response when validation fails.
    8. + *
    + *

    + * + *

    If validation succeeds, the method returns true, allowing the caller + * to proceed with the requested operation.

    + * + * @param authHeader The raw Authorization header sent by the client. + * @param requester The key object associated with the extracted token. + * + * @return 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 headers = clientRequest.getRequest().getHeaders(); + String authHeader = headers.get("Authorization"); // or headers.get("authorization") + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + // Extract the token by removing "Bearer " and trimming whitespace + token = authHeader.substring("Bearer ".length()).trim(); + } + if(!isModelAllowedForKey(model,token)){ + Logger.log("Not allowed to use", LogLevel.error); + continue; + } + return clientRequest; + } + } + return null; + } + */ + /** * Performs sequenced polling to optimize model sequencing and reduce model swaps. * @@ -405,6 +433,8 @@ private ClientRequest sequencedPolling(Request request) { return clientRequest; } + + /** * Checks whether the connection to the worker node is still open and valid. *