Config backup, a Docker dev/prod split, and a Gitea-built release image.
Added
- /settings -> General -> "Configuration backup": JSON export/import of the
panel's general settings + attribute definitions
(App\Services\ConfigBackupService); audited as config.export / config.import.
Export can leave out the attribute-encryption key. No AD data included.
- docker/compose.dev.yaml: live-reload dev stack that bind-mounts the working
tree. docker/php/Dockerfile is now multi-stage (base -> dev / prod).
- .github/workflows/release.yml: on every vX.Y.Z tag, builds the prod image and
pushes gitea.kzbikowski.pl/kzbkowski/adconsole:{X.Y.Z,X.Y,latest}.
Changed
- docker/compose.yaml: self-contained prod image (app code + composer install
--no-dev baked in via COPY); persistent state in docker/data/{app_storage,db};
defaults to the released image (ADCONSOLE_VERSION pins a tag, ADCONSOLE_IMAGE
replaces the ref). Apache now honours public/.htaccess (AllowOverride All).
Run from the repo root with --env-file .env.
- Repo root now holds only README.md; CHANGELOG / CONTRIBUTING / SECURITY /
CODE_OF_CONDUCT moved under docs/.
Upgrade notes: docs/upgrading.md#v110--docker-layout. Full log: docs/CHANGELOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKT9p3bfP8RZesExdYCS52
189 lines
7.4 KiB
PHP
189 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Exceptions\ConfigBackupException;
|
|
|
|
/**
|
|
* Eksport i import konfiguracji panelu (kopia zapasowa do pobrania):
|
|
* - ustawienia ogólne (wiersz 'panel' w `panel_settings`),
|
|
* - definicje atrybutów AD i kolumn listy (`panel_attributes`).
|
|
*
|
|
* Nie dotyka danych z Active Directory ani infrastruktury Laravela
|
|
* (sesje / cache / kolejki / dziennik `panel_audit`). Cały odczyt i zapis
|
|
* konfiguracji idzie przez PanelSettingsService - ta klasa tylko pakuje go
|
|
* do pliku JSON i z powrotem.
|
|
*/
|
|
class ConfigBackupService
|
|
{
|
|
/** Znacznik formatu pliku kopii zapasowej (pole `format`). */
|
|
public const FORMAT = 'adconsole-config';
|
|
|
|
/** Najwyższa obsługiwana wersja schematu pliku (pole `version`). */
|
|
public const SCHEMA_VERSION = 1;
|
|
|
|
public function __construct(protected PanelSettingsService $settings) {}
|
|
|
|
/**
|
|
* Buduje strukturę kopii zapasowej gotową do zapisania jako JSON.
|
|
*
|
|
* @param bool $includeEncryptionKey gdy false, klucz szyfrujący atrybuty
|
|
* nie trafia do pliku (bezpieczniejsze
|
|
* przy przesyłaniu / archiwizacji).
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function export(bool $includeEncryptionKey = true): array
|
|
{
|
|
$general = $this->settings->all();
|
|
|
|
if (! $includeEncryptionKey) {
|
|
$general['encryption']['key'] = '';
|
|
}
|
|
|
|
$keyIncluded = ($general['encryption']['key'] ?? '') !== '';
|
|
|
|
return [
|
|
'format' => self::FORMAT,
|
|
'version' => self::SCHEMA_VERSION,
|
|
'generator' => 'ADconsole',
|
|
'exported_at' => now()->toIso8601String(),
|
|
'encryption_key_included' => $keyIncluded,
|
|
'settings' => $general,
|
|
'attributes' => array_map(
|
|
static fn (array $a): array => [
|
|
'key' => $a['key'],
|
|
'label' => $a['label'],
|
|
'ldap' => $a['ldap'],
|
|
'category' => $a['category'],
|
|
'type' => $a['type'],
|
|
'regex' => $a['regex'],
|
|
'required' => $a['required'],
|
|
'help' => $a['help'],
|
|
'column' => $a['column'],
|
|
],
|
|
$this->settings->attributes(),
|
|
),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Waliduje i wgrywa kopię zapasową. Nadpisuje ustawienia ogólne i/lub
|
|
* listę atrybutów tym, co jest w pliku; sekcje nieobecne w pliku zostają
|
|
* bez zmian.
|
|
*
|
|
* @param mixed $data zdekodowana zawartość pliku (oczekiwana: tablica)
|
|
* @return array{settings: bool, attributes: int} co zostało wgrane
|
|
*
|
|
* @throws ConfigBackupException gdy plik jest pusty, w złym formacie albo
|
|
* pochodzi z nowszej wersji panelu
|
|
*/
|
|
public function import(mixed $data): array
|
|
{
|
|
if (! is_array($data)) {
|
|
throw new ConfigBackupException(__('config.backup.error_not_json'));
|
|
}
|
|
|
|
if (($data['format'] ?? null) !== self::FORMAT) {
|
|
throw new ConfigBackupException(__('config.backup.error_bad_format'));
|
|
}
|
|
|
|
$version = (int) ($data['version'] ?? 0);
|
|
if ($version < 1 || $version > self::SCHEMA_VERSION) {
|
|
throw new ConfigBackupException(__('config.backup.error_bad_version'));
|
|
}
|
|
|
|
$hasSettings = isset($data['settings']) && is_array($data['settings']);
|
|
$hasAttributes = isset($data['attributes']) && is_array($data['attributes']);
|
|
|
|
if (! $hasSettings && ! $hasAttributes) {
|
|
throw new ConfigBackupException(__('config.backup.error_empty'));
|
|
}
|
|
|
|
$applied = ['settings' => false, 'attributes' => 0];
|
|
|
|
if ($hasSettings) {
|
|
$this->settings->save($this->sanitizeSettings($data['settings']));
|
|
$applied['settings'] = true;
|
|
}
|
|
|
|
if ($hasAttributes) {
|
|
// saveAttributes() sam normalizuje wiersze, wymusza reguły pozycji
|
|
// systemowych / wyliczanych i odtwarza te, których w pliku brak.
|
|
$this->settings->saveAttributes($this->sanitizeAttributes($data['attributes']));
|
|
$applied['attributes'] = count($this->settings->attributes());
|
|
}
|
|
|
|
return $applied;
|
|
}
|
|
|
|
/**
|
|
* Sprowadza dowolne wejście do kształtu ustawień ogólnych: start od
|
|
* wbudowanych domyślnych, nadpisanie wyłącznie znanych kluczy, z kontrolą
|
|
* typu i zakresu. Odpowiada regułom z ConfigController::update(), żeby plik
|
|
* z ręcznie podmienioną wartością nie wprowadził panelu w zły stan.
|
|
*
|
|
* @param array<string, mixed> $in
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function sanitizeSettings(array $in): array
|
|
{
|
|
$d = $this->settings->defaults();
|
|
|
|
$reset = is_array($in['reset_password'] ?? null) ? $in['reset_password'] : [];
|
|
$policy = is_array($in['password_policy'] ?? null) ? $in['password_policy'] : [];
|
|
$enc = is_array($in['encryption'] ?? null) ? $in['encryption'] : [];
|
|
$audit = is_array($in['audit'] ?? null) ? $in['audit'] : [];
|
|
|
|
return [
|
|
'reset_password' => [
|
|
'must_change_password' => filter_var($reset['must_change_password'] ?? $d['reset_password']['must_change_password'], FILTER_VALIDATE_BOOL),
|
|
'unlock_account' => filter_var($reset['unlock_account'] ?? $d['reset_password']['unlock_account'], FILTER_VALIDATE_BOOL),
|
|
],
|
|
'password_policy' => [
|
|
'max_age_days' => max(0, min(3650, (int) ($policy['max_age_days'] ?? $d['password_policy']['max_age_days']))),
|
|
'generator_length' => max(8, min(128, (int) ($policy['generator_length'] ?? $d['password_policy']['generator_length']))),
|
|
'generator_symbols' => filter_var($policy['generator_symbols'] ?? $d['password_policy']['generator_symbols'], FILTER_VALIDATE_BOOL),
|
|
],
|
|
'encryption' => [
|
|
'key' => trim((string) ($enc['key'] ?? '')),
|
|
],
|
|
'audit' => [
|
|
'require_reason' => filter_var($audit['require_reason'] ?? $d['audit']['require_reason'], FILTER_VALIDATE_BOOL),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sprowadza listę atrybutów z pliku do wierszy przyjmowanych przez
|
|
* PanelSettingsService::saveAttributes() (ta klasa dokończy normalizację).
|
|
* Wiersze bez klucza są pomijane.
|
|
*
|
|
* @param array<int|string, mixed> $in
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
protected function sanitizeAttributes(array $in): array
|
|
{
|
|
$rows = [];
|
|
|
|
foreach ($in as $row) {
|
|
if (! is_array($row) || (string) ($row['key'] ?? '') === '') {
|
|
continue;
|
|
}
|
|
|
|
$rows[] = [
|
|
'key' => (string) ($row['key'] ?? ''),
|
|
'label' => (string) ($row['label'] ?? ''),
|
|
'ldap' => (string) ($row['ldap'] ?? ''),
|
|
'category' => (string) ($row['category'] ?? 'general'),
|
|
'type' => (string) ($row['type'] ?? 'text'),
|
|
'regex' => (string) ($row['regex'] ?? ''),
|
|
'required' => filter_var($row['required'] ?? false, FILTER_VALIDATE_BOOL),
|
|
'help' => (string) ($row['help'] ?? ''),
|
|
'column' => filter_var($row['column'] ?? false, FILTER_VALIDATE_BOOL),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
}
|