mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 20:23:26 +00:00
[Web] Rework API keys: multiple read-only/read-write keys, service keys from mailcow.conf
This commit is contained in:
21 files changed
+942
-335
No files matched your search
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey;
|
||||
|
||||
use Mailcow\ApiKey\Controller\ApiKeyApiController;
|
||||
use Mailcow\ApiKey\Repository\ApiKeyRepository;
|
||||
use Mailcow\ApiKey\Service\ApiKeyService;
|
||||
use Mailcow\Core\ApiResponse;
|
||||
use Mailcow\Core\Context;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Composition root of the API key module.
|
||||
*
|
||||
* process_add_return(ApiKeyModule::fromGlobals()->api->addKey($attr));
|
||||
* ApiKeyModule::create($pdo, Context::system())->keys->syncServiceKeys(getenv()); // php-fpm start
|
||||
*/
|
||||
class ApiKeyModule
|
||||
{
|
||||
/** Request keys whose values never reach a log */
|
||||
private const SECRET_KEYS = ['api_key'];
|
||||
|
||||
public function __construct(public readonly ApiKeyService $keys, public readonly ApiKeyApiController $api)
|
||||
{
|
||||
}
|
||||
|
||||
public static function create(PDO $pdo, Context $context): self
|
||||
{
|
||||
$keys = new ApiKeyService(new ApiKeyRepository($pdo), $context);
|
||||
return new self($keys, new ApiKeyApiController($keys, new ApiResponse(self::SECRET_KEYS)));
|
||||
}
|
||||
|
||||
/** Legacy entry point for procedural callers: reads $pdo and $_SESSION; $context overrides the session caller */
|
||||
public static function fromGlobals(?Context $context = null): self
|
||||
{
|
||||
return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey\Controller;
|
||||
|
||||
use Mailcow\ApiKey\Model\ApiKey;
|
||||
use Mailcow\ApiKey\Service\ApiKeyService;
|
||||
use Mailcow\Core\AccessDenied;
|
||||
use Mailcow\Core\ApiResponse;
|
||||
use Mailcow\Core\UserError;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* HTTP API adapter for json_api.php and edit.php (routes `api-key`).
|
||||
*
|
||||
* Write methods return what json_api passes to process_*_return() and write their messages via ApiResponse.
|
||||
* Messages are logged as ['api', action, 'key', data].
|
||||
*/
|
||||
class ApiKeyApiController
|
||||
{
|
||||
public function __construct(private ApiKeyService $keys, private ApiResponse $response)
|
||||
{
|
||||
}
|
||||
|
||||
/** POST add/api-key */
|
||||
public function addKey(array $attr): bool
|
||||
{
|
||||
return $this->handle('add', $attr,
|
||||
fn(array $log): bool => $this->response->success(['api_key_added', self::label($this->keys->create($attr))], $log));
|
||||
}
|
||||
|
||||
/** POST edit/api-key: one message per id */
|
||||
public function editKeys(?array $items, array $attr): bool
|
||||
{
|
||||
$data = array_merge(['id' => $items], $attr);
|
||||
return $this->handle('edit', $data, function (array $log) use ($data, $attr): bool {
|
||||
$this->keys->requireAdmin();
|
||||
return $this->batch((array)($data['id'] ?? []), $log,
|
||||
fn(int $id): array => ['api_key_modified', self::label($this->keys->update($id, $attr))]);
|
||||
});
|
||||
}
|
||||
|
||||
/** POST delete/api-key: one message per id */
|
||||
public function deleteKeys(array $items): bool
|
||||
{
|
||||
return $this->handle('delete', ['id' => $items], function (array $log) use ($items): bool {
|
||||
$this->keys->requireAdmin();
|
||||
return $this->batch($items, $log, fn(int $id): array => ['api_key_deleted', self::label($this->keys->delete($id))]);
|
||||
});
|
||||
}
|
||||
|
||||
/** GET api-key/all */
|
||||
public function getKeys(): array
|
||||
{
|
||||
return array_map(fn(ApiKey $key): array => $key->toArray(), $this->keys->all());
|
||||
}
|
||||
|
||||
/** GET api-key/<id>, also edit.php */
|
||||
public function getKey(mixed $id): array
|
||||
{
|
||||
$id = $this->toId($id);
|
||||
$key = ($id === null) ? null : $this->keys->get($id);
|
||||
return ($key === null) ? [] : $key->toArray();
|
||||
}
|
||||
|
||||
/** Name of a key in messages: its description, else its id */
|
||||
private static function label(ApiKey $key): string
|
||||
{
|
||||
return htmlspecialchars($key->description !== '' ? $key->description : (string)$key->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one write call: $work gets the log entry and returns the result for process_*_return().
|
||||
* A UserError becomes a danger message, a database error stops the call with one `mysql_error` message.
|
||||
*/
|
||||
private function handle(string $action, mixed $data, callable $work): bool
|
||||
{
|
||||
$log = ['api', $action, 'key', $data];
|
||||
try {
|
||||
$result = $work($log);
|
||||
} catch (UserError $e) {
|
||||
$result = $this->response->error($e, $log);
|
||||
} catch (PDOException $e) {
|
||||
$result = $this->response->danger(['mysql_error', $e->getMessage()], $log);
|
||||
}
|
||||
$this->response->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** One message per id: the success message returned by $work(id), or the UserError */
|
||||
private function batch(array $ids, array $log, callable $work): bool
|
||||
{
|
||||
foreach ($ids as $id) {
|
||||
try {
|
||||
$this->response->success($work($this->requireId($id)), $log);
|
||||
} catch (UserError $e) {
|
||||
$this->response->error($e, $log);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Integer id from request input ("12", 12); null for anything else */
|
||||
private function toId(mixed $id): ?int
|
||||
{
|
||||
return (is_numeric($id) && (float)$id == (int)(float)$id) ? (int)(float)$id : null;
|
||||
}
|
||||
|
||||
/** @throws AccessDenied for ids that cannot exist */
|
||||
private function requireId(mixed $id): int
|
||||
{
|
||||
return $this->toId($id) ?? throw new AccessDenied();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey\Model;
|
||||
|
||||
/**
|
||||
* An API key (table `api`), sent by clients in the X-API-Key header (see sessions.inc.php).
|
||||
*
|
||||
* Unlike other secrets the key is part of toArray(): admins copy it from the UI. ApiKeyService limits
|
||||
* read access to admins (not with a read-only API key).
|
||||
*/
|
||||
class ApiKey
|
||||
{
|
||||
public const ACCESS_RO = 'ro';
|
||||
public const ACCESS_RW = 'rw';
|
||||
|
||||
public ?int $id = null;
|
||||
public string $key = '';
|
||||
public string $description = '';
|
||||
public string $access = self::ACCESS_RO;
|
||||
/** @var string[] IPs and CIDR networks the key may be used from */
|
||||
public array $allowFrom = [];
|
||||
public int $skipIpCheck = 0;
|
||||
/** 1 = service key from mailcow.conf: shown, but neither editable nor deletable in UI and API */
|
||||
public int $service = 0;
|
||||
public int $active = 1;
|
||||
public ?string $created = null;
|
||||
public ?string $modified = null;
|
||||
|
||||
/** Builds a key from an `api` row (column names as in the table) */
|
||||
public static function fromRow(array $row): self
|
||||
{
|
||||
$key = new self();
|
||||
$key->id = isset($row['id']) ? (int)$row['id'] : null;
|
||||
$key->key = (string)($row['api_key'] ?? '');
|
||||
$key->description = (string)($row['description'] ?? '');
|
||||
$key->access = (string)($row['access'] ?? self::ACCESS_RO);
|
||||
$key->allowFrom = array_values(array_filter(array_map('trim', explode(',', (string)($row['allow_from'] ?? ''))), 'strlen'));
|
||||
$key->skipIpCheck = (int)($row['skip_ip_check'] ?? 0);
|
||||
$key->service = (int)($row['service'] ?? 0);
|
||||
$key->active = (int)($row['active'] ?? 1);
|
||||
$key->created = isset($row['created']) ? (string)$row['created'] : null;
|
||||
$key->modified = isset($row['modified']) ? (string)$row['modified'] : null;
|
||||
return $key;
|
||||
}
|
||||
|
||||
/** API representation (GET api-key) */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'description' => $this->description,
|
||||
'access' => $this->access,
|
||||
'api_key' => $this->key,
|
||||
'allow_from' => $this->allowFrom,
|
||||
'skip_ip_check' => $this->skipIpCheck,
|
||||
'service' => $this->service,
|
||||
'active' => $this->active,
|
||||
'created' => $this->created,
|
||||
'modified' => $this->modified,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey\Repository;
|
||||
|
||||
use Mailcow\ApiKey\Model\ApiKey;
|
||||
use Mailcow\Core\Repository;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQL for the API keys (table `api`).
|
||||
*/
|
||||
class ApiKeyRepository extends Repository
|
||||
{
|
||||
/** @return ApiKey[] service keys first, then by id */
|
||||
public function all(): array
|
||||
{
|
||||
return array_map([ApiKey::class, 'fromRow'],
|
||||
$this->query("SELECT * FROM `api` ORDER BY `service` DESC, `id`")->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function find(int $id): ?ApiKey
|
||||
{
|
||||
$row = $this->query("SELECT * FROM `api` WHERE `id` = :id", ['id' => $id])->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ? ApiKey::fromRow($row) : null;
|
||||
}
|
||||
|
||||
/** Inserts the key and returns the new id */
|
||||
public function insert(ApiKey $key): int
|
||||
{
|
||||
$key->id = $this->insertRow('api', $this->values($key) + ['service' => $key->service]);
|
||||
return $key->id;
|
||||
}
|
||||
|
||||
public function update(ApiKey $key): void
|
||||
{
|
||||
$this->updateRow('api', (int)$key->id, $this->values($key));
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->query("DELETE FROM `api` WHERE `id` = :id", ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the service keys match $keys in one transaction: other service keys are deleted,
|
||||
* an existing row with the same key (also a UI key) becomes the service key.
|
||||
*
|
||||
* @param ApiKey[] $keys
|
||||
*/
|
||||
public function syncService(array $keys): void
|
||||
{
|
||||
$this->transaction(function () use ($keys): void {
|
||||
$params = [];
|
||||
foreach (array_values($keys) as $i => $key) {
|
||||
$params['k' . $i] = $key->key;
|
||||
}
|
||||
$keep = empty($params) ? '' : " AND `api_key` NOT IN (:" . implode(', :', array_keys($params)) . ")";
|
||||
$this->query("DELETE FROM `api` WHERE `service` = '1'" . $keep, $params);
|
||||
// No INSERT ... ON DUPLICATE KEY UPDATE: it uses up an id on every php-fpm start
|
||||
foreach ($keys as $key) {
|
||||
$id = $this->query("SELECT `id` FROM `api` WHERE `api_key` = :api_key", ['api_key' => $key->key])->fetchColumn();
|
||||
if ($id === false) {
|
||||
$this->insertRow('api', $this->values($key) + ['service' => 1]);
|
||||
} else {
|
||||
$this->updateRow('api', (int)$id, $this->values($key) + ['service' => 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function values(ApiKey $key): array
|
||||
{
|
||||
return [
|
||||
'api_key' => $key->key,
|
||||
'description' => $key->description,
|
||||
'allow_from' => implode(',', $key->allowFrom),
|
||||
'skip_ip_check' => $key->skipIpCheck,
|
||||
'access' => $key->access,
|
||||
'active' => $key->active,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey\Service;
|
||||
|
||||
use Mailcow\ApiKey\Model\ApiKey;
|
||||
use Mailcow\ApiKey\Repository\ApiKeyRepository;
|
||||
use Mailcow\Core\AccessDenied;
|
||||
use Mailcow\Core\Context;
|
||||
use Mailcow\Core\Input;
|
||||
use Mailcow\Core\UserError;
|
||||
|
||||
/**
|
||||
* Use cases and rules for API keys.
|
||||
*
|
||||
* Admins only. A read-only API key may not even read the keys, since they include read-write keys.
|
||||
* Service keys are defined in mailcow.conf (API_KEY, API_KEY_READ_ONLY, API_ALLOW_FROM) and mirrored
|
||||
* into the table on every php-fpm start (syncServiceKeys()). UI and API show them, but cannot change them.
|
||||
*/
|
||||
class ApiKeyService
|
||||
{
|
||||
/** mailcow.conf variable => access of the service key it defines */
|
||||
public const SERVICE_KEYS = ['API_KEY' => ApiKey::ACCESS_RW, 'API_KEY_READ_ONLY' => ApiKey::ACCESS_RO];
|
||||
|
||||
public function __construct(private ApiKeyRepository $repo, private Context $context)
|
||||
{
|
||||
}
|
||||
|
||||
/** @throws AccessDenied unless an admin (login or read-write API key) */
|
||||
public function requireAdmin(): void
|
||||
{
|
||||
if (!$this->context->isAdmin() || $this->context->isReadOnlyApi()) {
|
||||
throw new AccessDenied();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return ApiKey[] all keys, service keys first; [] for other callers */
|
||||
public function all(): array
|
||||
{
|
||||
return $this->canRead() ? $this->repo->all() : [];
|
||||
}
|
||||
|
||||
public function get(int $id): ?ApiKey
|
||||
{
|
||||
return $this->canRead() ? $this->repo->find($id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key with a random value; `access` is required.
|
||||
*
|
||||
* @throws UserError
|
||||
*/
|
||||
public function create(array $input): ApiKey
|
||||
{
|
||||
$this->requireAdmin();
|
||||
$key = new ApiKey();
|
||||
$key->key = self::generate();
|
||||
$this->apply($key, $input);
|
||||
$this->repo->insert($key);
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a key; attributes missing in $input keep their value, `regen_key` issues a new key value.
|
||||
*
|
||||
* @throws UserError access_denied, api_key_service_locked, validation errors
|
||||
*/
|
||||
public function update(int $id, array $input): ApiKey
|
||||
{
|
||||
$this->requireAdmin();
|
||||
$key = $this->editable($id);
|
||||
$this->apply($key, $input);
|
||||
if (self::flag($input['regen_key'] ?? 0) === 1) {
|
||||
$key->key = self::generate();
|
||||
}
|
||||
$this->repo->update($key);
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a key and returns it.
|
||||
*
|
||||
* @throws UserError access_denied, api_key_service_locked
|
||||
*/
|
||||
public function delete(int $id): ApiKey
|
||||
{
|
||||
$this->requireAdmin();
|
||||
$key = $this->editable($id);
|
||||
$this->repo->delete($id);
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the service keys of mailcow.conf into the table. Runs on php-fpm start (init_db CLI, master only),
|
||||
* so there is no permission check. $env holds the container environment (getenv()).
|
||||
* Without a valid API_ALLOW_FROM no service key is active, like before. Service keys that were removed from
|
||||
* mailcow.conf are deleted; a UI key with the same value becomes the service key.
|
||||
*
|
||||
* @return string[] warnings for the container log
|
||||
*/
|
||||
public function syncServiceKeys(array $env): array
|
||||
{
|
||||
$warnings = [];
|
||||
$allowFrom = [];
|
||||
foreach (self::split($env['API_ALLOW_FROM'] ?? '') as $network) {
|
||||
// 'invalid' is the docker-compose default for an unset variable
|
||||
if ($network === 'invalid') {
|
||||
continue;
|
||||
}
|
||||
if (self::isNetwork($network)) {
|
||||
$allowFrom[] = $network;
|
||||
} else {
|
||||
$warnings[] = 'API_ALLOW_FROM: skipped invalid IP/network ' . $network;
|
||||
}
|
||||
}
|
||||
$keys = [];
|
||||
foreach (self::SERVICE_KEYS as $var => $access) {
|
||||
$value = trim((string)($env[$var] ?? ''));
|
||||
if ($value === '' || $value === 'invalid') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9-]+$/', $value)) {
|
||||
$warnings[] = $var . ': invalid characters (allowed: a-z, A-Z, 0-9, -), key ignored';
|
||||
continue;
|
||||
}
|
||||
if (empty($allowFrom)) {
|
||||
$warnings[] = $var . ': API_ALLOW_FROM contains no valid IP/network, key ignored';
|
||||
continue;
|
||||
}
|
||||
if (isset($keys[$value])) {
|
||||
$warnings[] = $var . ': same value as ' . $keys[$value]->description . ', key ignored';
|
||||
continue;
|
||||
}
|
||||
$key = new ApiKey();
|
||||
$key->key = $value;
|
||||
$key->description = $var;
|
||||
$key->access = $access;
|
||||
$key->allowFrom = $allowFrom;
|
||||
$key->service = 1;
|
||||
$keys[$value] = $key;
|
||||
}
|
||||
$this->repo->syncService(array_values($keys));
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
private function canRead(): bool
|
||||
{
|
||||
return $this->context->isAdmin() && !$this->context->isReadOnlyApi();
|
||||
}
|
||||
|
||||
/** @throws AccessDenied when missing, UserError api_key_service_locked for service keys */
|
||||
private function editable(int $id): ApiKey
|
||||
{
|
||||
$key = $this->repo->find($id) ?? throw new AccessDenied();
|
||||
if ($key->service === 1) {
|
||||
throw new UserError('api_key_service_locked', [$key->description]);
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/edit rules: attributes missing in $input keep the current value, a new key starts with the
|
||||
* model defaults. Without skip_ip_check the key needs at least one IP/network.
|
||||
*
|
||||
* @throws UserError
|
||||
*/
|
||||
private function apply(ApiKey $key, array $input): void
|
||||
{
|
||||
if (isset($input['description'])) {
|
||||
$description = Input::text($input['description']);
|
||||
if ($description === null || mb_strlen($description) > 255) {
|
||||
throw new UserError('api_key_description_invalid');
|
||||
}
|
||||
$key->description = $description;
|
||||
}
|
||||
if (isset($input['access']) || $key->id === null) {
|
||||
$access = $input['access'] ?? null;
|
||||
if (!in_array($access, [ApiKey::ACCESS_RO, ApiKey::ACCESS_RW], true)) {
|
||||
throw new UserError('api_key_access_invalid');
|
||||
}
|
||||
$key->access = $access;
|
||||
}
|
||||
if (isset($input['allow_from'])) {
|
||||
$key->allowFrom = self::networks($input['allow_from']);
|
||||
}
|
||||
if (isset($input['skip_ip_check'])) {
|
||||
$key->skipIpCheck = self::flag($input['skip_ip_check']);
|
||||
}
|
||||
if (isset($input['active'])) {
|
||||
$key->active = self::flag($input['active']);
|
||||
}
|
||||
if ($key->skipIpCheck === 0 && empty($key->allowFrom)) {
|
||||
throw new UserError('ip_list_empty');
|
||||
}
|
||||
}
|
||||
|
||||
/** New random key in the format of the legacy UI: five groups of six hex digits */
|
||||
private static function generate(): string
|
||||
{
|
||||
return implode('-', array_map(fn(): string => strtoupper(bin2hex(random_bytes(3))), range(1, 5)));
|
||||
}
|
||||
|
||||
/** Checkbox or API flag; a hidden "0" + checkbox pair arrives as ["0", "1"] when checked */
|
||||
private static function flag(mixed $value): int
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$value = end($value);
|
||||
}
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] validated IPs/networks
|
||||
* @throws UserError api_key_network_invalid for the first invalid entry
|
||||
*/
|
||||
private static function networks(mixed $value): array
|
||||
{
|
||||
$networks = self::split($value);
|
||||
foreach ($networks as $network) {
|
||||
if (!self::isNetwork($network)) {
|
||||
throw new UserError('api_key_network_invalid', [htmlspecialchars($network)]);
|
||||
}
|
||||
}
|
||||
return $networks;
|
||||
}
|
||||
|
||||
/** @return string[] unique entries of a list: array or string separated by spaces, commas, semicolons or newlines */
|
||||
private static function split(mixed $value): array
|
||||
{
|
||||
$values = is_array($value) ? $value : preg_split('/[\s,;]+/', is_scalar($value) ? (string)$value : '');
|
||||
$values = array_map(fn(mixed $v): string => is_scalar($v) ? trim((string)$v) : '', $values);
|
||||
return array_values(array_unique(array_filter($values, 'strlen')));
|
||||
}
|
||||
|
||||
/** IPv4/IPv6 address or CIDR network */
|
||||
private static function isNetwork(string $value): bool
|
||||
{
|
||||
[$ip, $prefix] = array_pad(explode('/', $value, 2), 2, null);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
|
||||
return false;
|
||||
}
|
||||
if ($prefix === null) {
|
||||
return true;
|
||||
}
|
||||
$max = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 32 : 128;
|
||||
return ctype_digit($prefix) && (int)$prefix <= $max;
|
||||
}
|
||||
}
|
||||
@@ -14,25 +14,32 @@ namespace Mailcow\Core;
|
||||
class Context
|
||||
{
|
||||
/**
|
||||
* @param array $acl ACL flags of the session (user_acl / da_acl, always set for admins)
|
||||
* @param bool $accessAll the session may ignore object ownership (`access_all_exception`)
|
||||
* @param array $acl ACL flags of the session (user_acl / da_acl, always set for admins)
|
||||
* @param bool $accessAll the session may ignore object ownership (`access_all_exception`)
|
||||
* @param ?string $apiAccess 'ro' / 'rw' when the request authenticated with an X-API-Key, else null
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $role,
|
||||
public readonly ?string $username,
|
||||
private readonly array $acl = [],
|
||||
public readonly bool $accessAll = false
|
||||
public readonly bool $accessAll = false,
|
||||
public readonly ?string $apiAccess = null
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromSession(array $session): self
|
||||
{
|
||||
$username = $session['mailcow_cc_username'] ?? null;
|
||||
$apiAccess = null;
|
||||
if (($session['mailcow_cc_api'] ?? false) === true) {
|
||||
$apiAccess = (($session['mailcow_cc_api_access'] ?? null) === 'rw') ? 'rw' : 'ro';
|
||||
}
|
||||
return new self(
|
||||
(string)($session['mailcow_cc_role'] ?? ''),
|
||||
is_scalar($username) ? (string)$username : null,
|
||||
is_array($session['acl'] ?? null) ? $session['acl'] : [],
|
||||
($session['access_all_exception'] ?? null) == '1'
|
||||
($session['access_all_exception'] ?? null) == '1',
|
||||
$apiAccess
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +64,12 @@ class Context
|
||||
return $this->role === 'user';
|
||||
}
|
||||
|
||||
/** Request with a read-only API key (role admin, but no write access) */
|
||||
public function isReadOnlyApi(): bool
|
||||
{
|
||||
return $this->apiAccess === 'ro';
|
||||
}
|
||||
|
||||
public function hasAcl(string $name): bool
|
||||
{
|
||||
return isset($this->acl[$name]) && $this->acl[$name] == '1';
|
||||
|
||||
@@ -29,6 +29,7 @@ Core/ shared by all modules
|
||||
```
|
||||
|
||||
Example: `Imapsync/` (sync jobs, sync sources, OAuth). Use it as the template for new modules.
|
||||
`ApiKey/` (API keys incl. the service keys of mailcow.conf) is a small module with the same layout.
|
||||
|
||||
## Layers and rules
|
||||
|
||||
|
||||
Reference in new issue
Block a user