Enhance BlacklistManager: add evidence parameter and UUID generation for blacklist entries

This commit is contained in:
netkas 2025-06-06 00:56:21 -04:00
parent fc6014b37e
commit f4536df74f
Signed by: netkas
GPG key ID: 4D8629441B76E4CC
7 changed files with 172 additions and 18 deletions

View file

@ -36,6 +36,7 @@
self::$configuration->setDefault('server.public_audit_entries', array_map(fn($type) => $type->value, AuditLogType::cases()));
self::$configuration->setDefault('server.public_evidence', true);
self::$configuration->setDefault('server.public_blacklist', true);
self::$configuration->setDefault('server.min_blacklist_time', 1800);
self::$configuration->setDefault('database.host', '127.0.0.1');
self::$configuration->setDefault('database.port', 3306);

View file

@ -23,6 +23,7 @@
private array $publicAuditEntries;
private bool $publicEvidence;
private bool $publicBlacklist;
private int $minBlacklistTime;
/**
* ServerConfiguration constructor.
@ -45,6 +46,7 @@
$this->publicAuditEntries = array_map(fn($type) => AuditLogType::from($type), $config['public_audit_entries'] ?? []);
$this->publicEvidence = $config['public_evidence'] ?? true;
$this->publicBlacklist = $config['public_blacklist'] ?? true;
$this->minBlacklistTime = $config['min_blacklist_time'] ?? 1800;
}
/**
@ -186,4 +188,16 @@
{
return $this->publicBlacklist;
}
/**
* Returns the minimum allowed time that a blacklist could be set to expire, for example
* 1800 = 30 Minutes, if a blacklist is set to expire within 30 minutes or more, it's valid, otherwise
* anything less than that if it isn't null would be considered invalid.
*
* @return int The number of seconds allowed
*/
public function getMinBlacklistTime(): int
{
return $this->minBlacklistTime;
}
}

View file

@ -4,11 +4,13 @@
use FederationServer\Classes\DatabaseConnection;
use FederationServer\Classes\Enums\BlacklistType;
use FederationServer\Classes\Validate;
use FederationServer\Exceptions\DatabaseOperationException;
use FederationServer\Objects\BlacklistRecord;
use InvalidArgumentException;
use PDO;
use PDOException;
use Symfony\Component\Uid\UuidV4;
class BlacklistManager
{
@ -19,10 +21,12 @@
* @param string $operator The UUID of the operator performing the blacklisting.
* @param BlacklistType $type The type of blacklist action.
* @param int|null $expires Optional expiration time in Unix timestamp, null for permanent blacklisting.
* @param string|null $evidence Optional evidence UUID, must be a valid UUID if provided.
* @return string The UUID of the created blacklist entry.
* @throws InvalidArgumentException If the entity or operator is empty, or if expires is in the past.
* @throws DatabaseOperationException If there is an error preparing or executing the SQL statement.
*/
public static function blacklistEntity(string $entity, string $operator, BlacklistType $type, ?int $expires = null): void
public static function blacklistEntity(string $entity, string $operator, BlacklistType $type, ?int $expires=null, ?string $evidence=null): string
{
if(empty($entity) || empty($operator))
{
@ -34,13 +38,22 @@
throw new InvalidArgumentException("Expiration time must be in the future or null for permanent blacklisting.");
}
if(!is_null($evidence) && !Validate::uuid($evidence))
{
throw new InvalidArgumentException("Evidence must be a valid UUID.");
}
$uuid = UuidV4::v4()->toRfc4122();
try
{
$stmt = DatabaseConnection::getConnection()->prepare("INSERT INTO blacklist (entity, operator, type, expires) VALUES (:entity, :operator, :type, :expires)");
$stmt = DatabaseConnection::getConnection()->prepare("INSERT INTO blacklist (uuid, entity, operator, type, expires, evidence) VALUES (:uuid, :entity, :operator, :type, :expires, :evidence)");
$stmt->bindParam(':uuid', $uuid);
$type = $type->value;
$stmt->bindParam(':entity', $entity);
$stmt->bindParam(':operator', $operator);
$stmt->bindParam(':type', $type);
$stmt->bindParam(':evidence', $evidence);
// Convert expires to datetime
if(is_null($expires))
@ -56,6 +69,8 @@
{
throw new DatabaseOperationException("Failed to prepare SQL statement for blacklisting entity: " . $e->getMessage(), 0, $e);
}
return $uuid;
}
/**

View file

@ -2,6 +2,7 @@
namespace FederationServer\Classes\Managers;
use FederationServer\Classes\Configuration;
use FederationServer\Classes\DatabaseConnection;
use FederationServer\Classes\Utilities;
use FederationServer\Exceptions\DatabaseOperationException;
@ -53,6 +54,77 @@
return $uuid;
}
/**
* Creates the master operator with a predefined API key.
*
* @param string $apiKey The API key for the master operator.
* @return string The UUID of the created master operator.
* @throws DatabaseOperationException If there is an error during the database operation.
*/
private static function createMasterOperator(string $apiKey): string
{
if(empty($apiKey))
{
throw new InvalidArgumentException('API key cannot be empty.');
}
if(strlen($apiKey) !== 32)
{
throw new InvalidArgumentException('API key must be exactly 32 characters long.');
}
// This method is used to create the master operator with a predefined API key.
// It should only be called once during the initial setup of the server.
$uuid = Uuid::v7()->toRfc4122();
try
{
$stmt = DatabaseConnection::getConnection()->prepare("INSERT INTO operators (uuid, api_key, name, manage_operators, manage_blacklist, is_client) VALUES (:uuid, :api_key, 'root', 1, 1, 1)");
$stmt->bindParam(':uuid', $uuid);
$stmt->bindParam(':api_key', $apiKey);
$stmt->execute();
}
catch (PDOException $e)
{
throw new DatabaseOperationException('Failed to create master operator', 0, $e);
}
return $uuid;
}
/**
* Retrieve the master operator.
*
* This method checks if the master operator exists in the database.
* If it does not exist, it creates one with a predefined API key.
*
* @return OperatorRecord The master operator record.
* @throws DatabaseOperationException If there is an error during the database operation.
* @throws InvalidArgumentException If the API key for the master operator is not set in the configuration.
*/
public static function getMasterOperator(): OperatorRecord
{
// This method retrieves the master operator from the database.
// If the master operator does not exist, it creates one with a predefined API key.
$apiKey = Configuration::getServerConfiguration()->getApiKey();
if(empty($apiKey))
{
throw new InvalidArgumentException('API key for master operator is not set in configuration.');
}
$operator = self::getOperatorByApiKey($apiKey);
if($operator === null)
{
$uuid = self::createMasterOperator($apiKey);
$operator = self::getOperator($uuid);
}
return $operator;
}
/**
* Retrieve an operator by their UUID.
*

View file

@ -8,6 +8,7 @@
use FederationServer\Interfaces\RequestHandlerInterface;
use FederationServer\Interfaces\SerializableInterface;
use FederationServer\Objects\OperatorRecord;
use InvalidArgumentException;
use Throwable;
abstract class RequestHandler implements RequestHandlerInterface
@ -207,23 +208,52 @@
*/
protected static function getAuthenticatedOperator(): ?OperatorRecord
{
// First obtain the API key from the request headers or query parameters.
$apiKey = $_SERVER['HTTP_API_KEY'] ?? $_GET['api_key'] ?? $_POST['api_key'] ?? null;
$apiKey = null;
if (isset($_SERVER['HTTP_AUTHORIZATION']))
{
$authHeader = $_SERVER['HTTP_AUTHORIZATION'];
if (preg_match('/^Bearer\s+(\S+)$/', $authHeader, $matches))
{
$apiKey = $matches[1];
}
}
if (empty($apiKey))
{
return null;
}
if(strlen($apiKey) > 32)
if (strlen($apiKey) !== 32)
{
throw new RequestException('API key is too long', 400);
throw new RequestException('Invalid API key', 400);
}
// If the given API key matches the master operator's API key, we can retrieve the master operator.
if(Configuration::getServerConfiguration()->getApiKey() !== null && $apiKey === Configuration::getServerConfiguration()->getApiKey())
{
// A master operator is automatically created if it does not exist.
// This is useful for initial setup or if the master operator was deleted.
// Master operators cannot be disabled, so we can safely return it.
try
{
return OperatorManager::getMasterOperator();
}
catch (DatabaseOperationException $e)
{
throw new RequestException('Internal Database Error', 500, $e);
}
catch(InvalidArgumentException $e)
{
throw new RequestException('Invalid API Key Configuration', 500, $e);
}
}
try
{
$operator = OperatorManager::getOperatorByApiKey($apiKey);
if($operator === null)
if ($operator === null)
{
throw new RequestException('Invalid API key', 401);
}
@ -238,7 +268,6 @@
throw new RequestException('Operator is disabled', 403);
}
// If the operator is found and enabled, return the OperatorRecord object.
return $operator;
}
}

View file

@ -1,11 +1,13 @@
create table operators
(
uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier for the operator'
primary key comment 'The Unique Primary Index for the operator UUID',
uuid varchar(36) default uuid() not null comment 'The Unique Primary Index for the operator UUID'
primary key,
name varchar(32) not null comment 'The public name of the operator',
api_key varchar(32) not null comment 'The current API key of the operator',
manage_operators tinyint(1) default 0 not null comment 'Default: 0, 1=This operator can manage other operators by creating new ones, deleting existing ones or disabling existing ones, etc. 0=No such permissions are allowed',
manage_blacklist tinyint default 0 not null comment 'Default: 0, 1=This operator can manage the blacklist by adding/removing to the database, 0=No such permissions are allowed',
is_client tinyint default 0 not null comment 'Default: 0, 1=This operator has access to client methods that allows the client to build the database of known entities and automatically report evidence or manage the database (if permitted to do so), 0=No such permissions are allowed',
disabled tinyint(1) default 0 not null comment 'Default: 0, 1=The operator is disabled, 0=The oprator is active',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this operator record was created',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this operator record was last updated',
constraint operators_api_key_uindex
@ -16,8 +18,8 @@ create table operators
create definer = root@localhost trigger operators_update
before update
on operators
for each row
on operators
for each row
BEGIN
SET NEW.updated = CURRENT_TIMESTAMP;
END;

View file

@ -117,16 +117,37 @@
}
/**
* @inheritDoc
* Get the currently authenticated operator.
*
* This method retrieves the currently authenticated operator, if any.
* If no operator is authenticated, it returns null.
*
* @param bool $requireAuthentication Whether to require authentication. Defaults to true.
* @return OperatorRecord|null The authenticated operator record or null if not authenticated.
* @throws RequestException If authentication is provided but is invalid/operator is disabled.
*/
public static function getAuthenticatedOperator(bool $requireAuthentication=true): ?OperatorRecord
{
$authenticatedOperator = parent::getAuthenticatedOperator();
if($requireAuthentication && $authenticatedOperator === null)
return parent::getAuthenticatedOperator();
}
/**
* Get the authenticated operator, throwing an exception if not authenticated.
*
* This method retrieves the currently authenticated operator. If no operator is authenticated,
* it throws a RequestException with a 401 Unauthorized status code.
*
* @return OperatorRecord The authenticated operator record.
* @throws RequestException If no operator is authenticated.
*/
public static function requireAuthenticatedOperator(): OperatorRecord
{
$operator = self::getAuthenticatedOperator();
if ($operator === null)
{
throw new RequestException('Unauthorized: No authenticated operator found', 401);
throw new RequestException('Authentication required', 401);
}
return $authenticatedOperator;
return $operator;
}
}