adasdadad
All checks were successful
Deploy / deploy-staging (push) Successful in 26s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-26 00:54:30 +02:00
parent a70704bc0a
commit 5a488260b9
17 changed files with 678 additions and 76 deletions

View File

@@ -24,7 +24,8 @@ final class AdminAppsService
require_once AppPaths::systemAddonPath($this->projectRoot, 'gitea-deploy-status') . '/GiteaDeployStatusService.php';
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
$appRegistry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
$accessOverrides = AppAccessConfig::load($this->projectRoot);
$appRegistry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps(), $accessOverrides);
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
$apps = $this->normalizeApps($appRegistry->all());
@@ -37,6 +38,8 @@ final class AdminAppsService
'meta' => [
'environment' => $currentEnvironment,
'gitea_config_path' => $this->environmentConfigPath('gitea', $currentEnvironment),
'app_access_config_path' => $this->environmentConfigPath('app_access', $currentEnvironment),
'available_groups' => AppAccessConfig::availableGroups($this->projectRoot),
],
'stats' => [
'apps_total' => count($apps),
@@ -85,6 +88,15 @@ final class AdminAppsService
return $config;
}
/**
* @param array<int, string> $requiredGroups
* @return array<string, array{required_groups: array<int, string>}>
*/
public function saveAppAccess(string $appId, array $requiredGroups): array
{
return AppAccessConfig::saveRequiredGroups($this->projectRoot, $appId, $requiredGroups);
}
/**
* @param array<int, array<string, mixed>> $apps
* @return array<int, array<string, mixed>>
@@ -107,6 +119,7 @@ final class AdminAppsService
'supports_tray' => !empty($app['supports_tray']),
'enabled_by_default' => !empty($app['enabled_by_default']),
'required_groups' => $requiredGroups,
'required_groups_display' => $requiredGroups === [] ? 'Alle sichtbaren Benutzer' : implode(', ', $requiredGroups),
'admin_only' => in_array('administrators', array_map(
static fn (string $group): string => strtolower(trim($group, '/')),
$requiredGroups

View File

@@ -31,7 +31,11 @@ final class App
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
$auth = new KeycloakAuth($keycloakConfig);
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
$registry = new AppRegistry(
$this->projectRoot . '/config/apps.php',
$moduleRegistry->desktopApps(),
AppAccessConfig::load($this->projectRoot)
);
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
$giteaStatus = new \SystemAddons\GiteaDeployStatus\GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
@@ -40,7 +44,10 @@ final class App
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
? $_SESSION['desktop_auth']['user']
: [];
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
$authGroups = array_values(array_map(
static fn (string $group): string => strtolower(trim($group, '/')),
array_map('strval', (array) ($authUser['groups'] ?? []))
));
$isAdmin = in_array('administrators', $authGroups, true);
$visibleApps = AppVisibility::filterByGroups($registry->all(), $authGroups);
$userSelfManagement = new UserSelfManagementService($this->projectRoot);
@@ -53,22 +60,47 @@ final class App
);
$userSettings = $resolvedSkin['settings'];
$activeSkin = (string) $resolvedSkin['skin'];
$enabledApps = AppVisibility::filterByEnabledIds(
$menuAppIds = array_values(array_map('strval', (array) ($userSettings['apps']['enabled_ids'] ?? [])));
$desktopIconIds = array_values(array_map('strval', (array) ($userSettings['apps']['desktop_icon_ids'] ?? [])));
$trayAppIds = array_values(array_map('strval', (array) ($userSettings['tray']['active_ids'] ?? [])));
$visibleAppIds = array_values(array_filter(array_map(
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
$visibleApps
)));
$availableWidgets = $widgetRegistry->availableForApps($visibleAppIds);
$activeWidgetIds = array_values(array_map('strval', (array) ($userSettings['widgets']['active_ids'] ?? DesktopState::defaultWidgetIds())));
$activeWidgets = $widgetRegistry->active($activeWidgetIds, $visibleAppIds);
$widgetLaunchAppIds = array_values(array_filter(array_unique(array_map(
static fn (array $widget): string => (string) (($widget['launch_app_id'] ?? '') ?: ($widget['source_app_id'] ?? '')),
$activeWidgets
))));
$payloadAppIds = array_values(array_unique(array_filter(array_merge(
$menuAppIds,
$desktopIconIds,
$trayAppIds,
$widgetLaunchAppIds,
[UserSelfManagementService::APP_ID]
))));
$payloadApps = AppVisibility::filterByEnabledIds(
$visibleApps,
array_values(array_map('strval', (array) ($userSettings['apps']['enabled_ids'] ?? []))),
$payloadAppIds,
[UserSelfManagementService::APP_ID]
);
$apps = AppIconResolver::decorate($enabledApps, $activeSkin);
$apps = AppIconResolver::decorate(array_map(static function (array $app) use ($menuAppIds, $desktopIconIds, $trayAppIds): array {
$appId = (string) ($app['app_id'] ?? '');
$installable = !array_key_exists('installable', $app) || (bool) $app['installable'];
$app['user_show_in_start_menu'] = $installable
? in_array($appId, $menuAppIds, true)
: (($app['show_in_start_menu'] ?? true) !== false);
$app['user_show_on_desktop'] = in_array($appId, $desktopIconIds, true);
$app['user_show_in_tray'] = in_array($appId, $trayAppIds, true);
return $app;
}, $payloadApps), $activeSkin);
$windows = (new WindowManager($registry))->defaultWindows();
$enabledAppIds = array_values(array_filter(array_map(
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
$enabledApps
)));
$availableWidgets = $widgetRegistry->availableForApps($enabledAppIds);
$activeWidgetIds = array_values(array_map('strval', (array) ($userSettings['widgets']['active_ids'] ?? DesktopState::defaultWidgetIds())));
$trayItems = DesktopState::trayItems();
foreach ($enabledApps as $app) {
if (empty($app['supports_tray']) || (string) ($app['app_id'] ?? '') !== 'fx-rates') {
foreach ($apps as $app) {
if (empty($app['supports_tray']) || empty($app['user_show_in_tray'])) {
continue;
}
@@ -120,7 +152,7 @@ final class App
'apps' => $apps,
'windows' => $windows,
'widgets' => [
'active' => $widgetRegistry->active($activeWidgetIds, $enabledAppIds),
'active' => $activeWidgets,
'registry' => $availableWidgets,
'active_ids' => $activeWidgetIds,
],

161
src/App/AppAccessConfig.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App;
final class AppAccessConfig
{
/**
* @return array<string, array{required_groups: array<int, string>}>
*/
public static function load(string $projectRoot): array
{
$config = ConfigLoader::load($projectRoot, 'app_access');
$normalized = [];
foreach ($config as $appId => $definition) {
$appId = trim((string) $appId);
if ($appId === '' || !is_array($definition)) {
continue;
}
$normalized[$appId] = [
'required_groups' => self::sanitizeGroups($definition['required_groups'] ?? []),
];
}
return $normalized;
}
/**
* @return array<int, array{id: string, label: string, dn: string}>
*/
public static function availableGroups(string $projectRoot): array
{
$registration = ConfigLoader::load($projectRoot, 'registration');
$groups = [];
foreach ((array) ($registration['ldap']['available_groups'] ?? []) as $entry) {
if (!is_array($entry)) {
continue;
}
$dn = trim((string) ($entry['dn'] ?? ''));
$label = trim((string) ($entry['label'] ?? ''));
$id = self::ldapGroupId($dn);
if ($id === '') {
continue;
}
$groups[$id] = [
'id' => $id,
'label' => $label !== '' ? $label : $id,
'dn' => $dn,
];
}
ksort($groups);
return array_values($groups);
}
/**
* @param array<int, string> $requiredGroups
* @return array<string, array{required_groups: array<int, string>}>
*/
public static function saveRequiredGroups(string $projectRoot, string $appId, array $requiredGroups): array
{
$appId = trim($appId);
if ($appId === '') {
throw new \RuntimeException('App-ID fehlt.');
}
$current = self::load($projectRoot);
$current[$appId] = [
'required_groups' => self::sanitizeGroups($requiredGroups),
];
self::writeEnvironmentConfig($projectRoot, 'app_access', ConfigLoader::currentEnvironment(), $current);
return $current;
}
private static function ldapGroupId(string $dn): string
{
if ($dn === '') {
return '';
}
if (preg_match('/cn=([^,]+)/i', $dn, $matches) !== 1) {
return '';
}
return strtolower(trim((string) ($matches[1] ?? '')));
}
/**
* @param mixed $groups
* @return array<int, string>
*/
private static function sanitizeGroups(mixed $groups): array
{
$result = [];
foreach ((array) $groups as $group) {
$group = strtolower(trim((string) $group, " /\t\n\r\0\x0B"));
if ($group === '') {
continue;
}
$result[$group] = $group;
}
return array_values($result);
}
/**
* @param array<string, mixed> $config
*/
private static function writeEnvironmentConfig(string $projectRoot, string $name, string $environment, array $config): void
{
$directory = rtrim($projectRoot, '/') . '/config/' . $environment;
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new \RuntimeException('Konfigurationsverzeichnis konnte nicht angelegt werden.');
}
$path = $directory . '/' . $name . '.php';
$content = "<?php\n\n";
$content .= "declare(strict_types=1);\n\n";
$content .= 'return ' . self::exportPhpValue($config) . ";\n";
if (file_put_contents($path, $content) === false) {
throw new \RuntimeException('Konfigurationsdatei konnte nicht geschrieben werden.');
}
}
private static function exportPhpValue(mixed $value, int $depth = 0): string
{
if (is_array($value)) {
if ($value === []) {
return '[]';
}
$indent = str_repeat(' ', $depth);
$childIndent = str_repeat(' ', $depth + 1);
$lines = ['['];
foreach ($value as $key => $item) {
$prefix = is_int($key) ? '' : var_export($key, true) . ' => ';
$lines[] = $childIndent . $prefix . self::exportPhpValue($item, $depth + 1) . ',';
}
$lines[] = $indent . ']';
return implode("\n", $lines);
}
return var_export($value, true);
}
}

View File

@@ -105,21 +105,30 @@ final class UserSelfManagementService
{
$settings = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
$enabledAppIds = array_values(array_map('strval', (array) ($settings['apps']['enabled_ids'] ?? [])));
$enabledAppLookup = array_fill_keys($enabledAppIds, true);
$desktopIconIds = array_values(array_map('strval', (array) ($settings['apps']['desktop_icon_ids'] ?? [])));
$trayAppIds = array_values(array_map('strval', (array) ($settings['tray']['active_ids'] ?? [])));
$visibleAppLookup = array_fill_keys(array_values(array_filter(array_map(
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
$visibleApps
))), true);
$activeWidgetIds = array_values(array_map('strval', (array) ($settings['widgets']['active_ids'] ?? [])));
$installableApps = array_values(array_filter(
$visibleApps,
static fn (array $app): bool => !array_key_exists('installable', $app) || (bool) $app['installable']
));
$trayApps = array_values(array_filter(
$visibleApps,
static fn (array $app): bool => !empty($app['supports_tray'])
));
$availableWidgets = array_values(array_filter(
$widgets,
static function (array $widget) use ($enabledAppLookup): bool {
static function (array $widget) use ($visibleAppLookup): bool {
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
if ($sourceAppId === '') {
return true;
}
return isset($enabledAppLookup[$sourceAppId]);
return isset($visibleAppLookup[$sourceAppId]);
}
));
@@ -142,9 +151,20 @@ final class UserSelfManagementService
'app_scope' => (string) ($app['app_scope'] ?? 'module'),
'required' => in_array((string) ($app['app_id'] ?? ''), [self::APP_ID], true),
'enabled' => in_array((string) ($app['app_id'] ?? ''), $enabledAppIds, true),
'can_show_on_desktop' => ($app['show_on_desktop'] ?? false) !== false,
'desktop_icon_enabled' => in_array((string) ($app['app_id'] ?? ''), $desktopIconIds, true),
],
$installableApps
),
'tray_apps' => array_map(
static fn (array $app): array => [
'app_id' => (string) ($app['app_id'] ?? ''),
'title' => (string) ($app['title'] ?? ''),
'summary' => (string) ($app['summary'] ?? ''),
'enabled' => in_array((string) ($app['app_id'] ?? ''), $trayAppIds, true),
],
$trayApps
),
'widgets' => array_map(
static fn (array $widget): array => [
'widget_id' => (string) ($widget['widget_id'] ?? ''),
@@ -153,6 +173,7 @@ final class UserSelfManagementService
'default_enabled' => (bool) ($widget['default_enabled'] ?? false),
'enabled' => in_array((string) ($widget['widget_id'] ?? ''), $activeWidgetIds, true),
'source_app_id' => (string) ($widget['source_app_id'] ?? ''),
'widget_type' => (string) ($widget['widget_type'] ?? 'default'),
],
$availableWidgets
),
@@ -220,12 +241,44 @@ final class UserSelfManagementService
static fn (array $app): ?string => ($app['enabled_by_default'] ?? false) ? (string) ($app['app_id'] ?? '') : null,
$visibleApps
)));
$desktopIconAllowedIds = array_values(array_filter(array_map(
static fn (array $app): ?string => ((!array_key_exists('installable', $app) || (bool) $app['installable']) && (($app['show_on_desktop'] ?? false) !== false))
? (string) ($app['app_id'] ?? '')
: null,
$visibleApps
)));
$defaultDesktopIconIds = array_values(array_filter(array_map(
static fn (array $app): ?string => ((!array_key_exists('installable', $app) || (bool) $app['installable']) && !empty($app['show_on_desktop']))
? (string) ($app['app_id'] ?? '')
: null,
$visibleApps
)));
$trayAllowedIds = array_values(array_filter(array_map(
static fn (array $app): ?string => !empty($app['supports_tray']) ? (string) ($app['app_id'] ?? '') : null,
$visibleApps
)));
$defaultTrayIds = array_values(array_filter(array_map(
static fn (array $app): ?string => (!empty($app['supports_tray']) && !empty($app['enabled_by_default'])) ? (string) ($app['app_id'] ?? '') : null,
$visibleApps
)));
$defaultWidgetIds = array_values(array_filter(array_map(
static fn (array $widget): ?string => ($widget['default_enabled'] ?? false) ? (string) ($widget['widget_id'] ?? '') : null,
$widgets
)));
$allWidgetIds = array_values(array_filter(array_map(
static fn (array $widget): string => (string) ($widget['widget_id'] ?? ''),
$visibleWidgetIds = array_values(array_filter(array_map(
static function (array $widget) use ($visibleAppIds): ?string {
$widgetId = (string) ($widget['widget_id'] ?? '');
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
if ($widgetId === '') {
return null;
}
if ($sourceAppId !== '' && !in_array($sourceAppId, $visibleAppIds, true)) {
return null;
}
return $widgetId;
},
$widgets
)));
$storedEnabledAppIds = array_key_exists('enabled_ids', (array) ($settings['apps'] ?? []))
@@ -244,12 +297,34 @@ final class UserSelfManagementService
$visibleAppIds,
[self::APP_ID]
);
$storedDesktopIconIds = array_key_exists('desktop_icon_ids', (array) ($settings['apps'] ?? []))
? $this->sanitizeSelection($settings['apps']['desktop_icon_ids'] ?? [], $desktopIconAllowedIds)
: $defaultDesktopIconIds;
$desktopIconIds = $this->sanitizeSelection(
$storedDesktopIconIds,
array_values(array_intersect($desktopIconAllowedIds, $enabledAppIds))
);
$storedTrayIds = array_key_exists('active_ids', (array) ($settings['tray'] ?? []))
? $this->sanitizeSelection($settings['tray']['active_ids'] ?? [], $trayAllowedIds)
: $defaultTrayIds;
$knownTrayIds = $this->sanitizeSelection(
$settings['tray']['known_ids'] ?? [],
$trayAllowedIds
);
$newDefaultTrayIds = array_values(array_filter(
$defaultTrayIds,
static fn (string $appId): bool => !in_array($appId, $knownTrayIds, true)
));
$trayIds = $this->sanitizeSelection(
array_merge($storedTrayIds, $newDefaultTrayIds),
$trayAllowedIds
);
$storedActiveWidgetIds = array_key_exists('active_ids', (array) ($settings['widgets'] ?? []))
? $this->sanitizeSelection($settings['widgets']['active_ids'] ?? [], $allWidgetIds)
? $this->sanitizeSelection($settings['widgets']['active_ids'] ?? [], $visibleWidgetIds)
: $defaultWidgetIds;
$knownWidgetIds = $this->sanitizeSelection(
$settings['widgets']['known_ids'] ?? [],
$allWidgetIds
$visibleWidgetIds
);
$newDefaultWidgetIds = array_values(array_filter(
$defaultWidgetIds,
@@ -257,14 +332,14 @@ final class UserSelfManagementService
));
$activeWidgetIds = $this->sanitizeSelection(
array_merge($storedActiveWidgetIds, $newDefaultWidgetIds),
array_values(array_filter($allWidgetIds, function (string $widgetId) use ($widgets, $enabledAppIds): bool {
array_values(array_filter($visibleWidgetIds, function (string $widgetId) use ($widgets, $visibleAppIds): bool {
foreach ($widgets as $widget) {
if ((string) ($widget['widget_id'] ?? '') !== $widgetId) {
continue;
}
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
return $sourceAppId === '' || in_array($sourceAppId, $enabledAppIds, true);
return $sourceAppId === '' || in_array($sourceAppId, $visibleAppIds, true);
}
return false;
@@ -297,11 +372,16 @@ final class UserSelfManagementService
],
'apps' => [
'enabled_ids' => $enabledAppIds,
'desktop_icon_ids' => $desktopIconIds,
'known_ids' => $visibleAppIds,
],
'tray' => [
'active_ids' => $trayIds,
'known_ids' => $trayAllowedIds,
],
'widgets' => [
'active_ids' => $activeWidgetIds,
'known_ids' => $allWidgetIds,
'known_ids' => $visibleWidgetIds,
],
'integration' => [
'ldap_write_status' => 'planned',

View File

@@ -11,12 +11,27 @@ final class AppRegistry
/**
* @param array<int, array<string, mixed>> $additionalApps
* @param array<string, array<string, mixed>> $accessOverrides
*/
public function __construct(string $configPath, array $additionalApps = [])
public function __construct(string $configPath, array $additionalApps = [], array $accessOverrides = [])
{
/** @var array<int, array<string, mixed>> $apps */
$apps = require $configPath;
$this->apps = array_values(array_merge($apps, $additionalApps));
$mergedApps = array_values(array_merge($apps, $additionalApps));
$this->apps = array_map(static function (array $app) use ($accessOverrides): array {
$appId = (string) ($app['app_id'] ?? '');
if ($appId === '') {
return $app;
}
$override = $accessOverrides[$appId] ?? null;
if (is_array($override) && array_key_exists('required_groups', $override)) {
$app['required_groups'] = array_values(array_map('strval', (array) ($override['required_groups'] ?? [])));
}
return $app;
}, $mergedApps);
}
/**