refactor: integration labels, AdminPostAction, ExposurePolicy key parsing (M9)
Integrations name their own admin group (Integration::label, registry labels()); ToolsTab no longer hard-codes ids and explains why it re-applies withGroups. AccessTab and LogTab share AdminPostAction (guard, redirect, terminate seam). ExposurePolicy parses option keys in one place, has one isRequired() check and a clear $groups docblock (null vs []). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -47,7 +47,12 @@ final class ExposurePolicy
|
||||
/** @var array<string, true> Group ids switched to "nur lesen". */
|
||||
private array $readonlyGroups = [];
|
||||
|
||||
/** @var array<string, string>|null Ability name => group id; null until resolved. */
|
||||
/**
|
||||
* Ability name => group id. [] = no groups known (withGroups() not called: group entries have
|
||||
* no effect); null = a lazy $groupSource is set and not resolved yet (groupOf() resolves it).
|
||||
*
|
||||
* @var array<string, string>|null
|
||||
*/
|
||||
private ?array $groups = [];
|
||||
|
||||
/** @var (callable(): array<string, string>)|null Lazy source of $groups. */
|
||||
@@ -60,17 +65,17 @@ final class ExposurePolicy
|
||||
public function __construct(array $overrides = [])
|
||||
{
|
||||
foreach ($overrides as $key => $value) {
|
||||
$key = (string) $key;
|
||||
if (str_starts_with($key, self::GROUP_PREFIX)) {
|
||||
['kind' => $kind, 'id' => $id] = self::parseKey((string) $key);
|
||||
if ($kind === 'group') {
|
||||
if (is_string($value) && in_array($value, self::MODES, true)) {
|
||||
$this->groupModes[substr($key, strlen(self::GROUP_PREFIX))] = $value;
|
||||
$this->groupModes[$id] = $value;
|
||||
}
|
||||
} elseif (str_starts_with($key, self::READONLY_PREFIX)) {
|
||||
} elseif ($kind === 'readonly') {
|
||||
if ($value === true) {
|
||||
$this->readonlyGroups[substr($key, strlen(self::READONLY_PREFIX))] = true;
|
||||
$this->readonlyGroups[$id] = true;
|
||||
}
|
||||
} elseif (is_string($value)) {
|
||||
$this->tools[$key] = $value;
|
||||
$this->tools[$id] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +108,7 @@ final class ExposurePolicy
|
||||
|
||||
public function mode(Ability $ability): string
|
||||
{
|
||||
if (in_array($ability->name(), self::REQUIRED, true)) {
|
||||
if (self::isRequired($ability->name())) {
|
||||
return $this->defaultMode($ability);
|
||||
}
|
||||
|
||||
@@ -119,7 +124,7 @@ final class ExposurePolicy
|
||||
{
|
||||
$name = $ability->name();
|
||||
$groupMode = $this->groupModeOf($name);
|
||||
if ($groupMode === null || in_array($name, self::REQUIRED, true)) {
|
||||
if ($groupMode === null || self::isRequired($name)) {
|
||||
return $this->defaultMode($ability);
|
||||
}
|
||||
|
||||
@@ -133,7 +138,7 @@ final class ExposurePolicy
|
||||
$groupMode = $this->groupModeOf($name);
|
||||
|
||||
return $groupMode !== null
|
||||
&& ! in_array($name, self::REQUIRED, true)
|
||||
&& ! self::isRequired($name)
|
||||
&& ! self::isAllowed($name, $groupMode);
|
||||
}
|
||||
|
||||
@@ -145,7 +150,7 @@ final class ExposurePolicy
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($ability->name(), self::REQUIRED, true)) {
|
||||
if (self::isRequired($ability->name())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,7 +218,7 @@ final class ExposurePolicy
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($name, self::REQUIRED, true)) {
|
||||
if (self::isRequired($name)) {
|
||||
return $mode === self::DIRECT;
|
||||
}
|
||||
|
||||
@@ -244,17 +249,18 @@ final class ExposurePolicy
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($key, self::GROUP_PREFIX)) {
|
||||
['kind' => $kind, 'id' => $id] = self::parseKey($key);
|
||||
if ($kind === 'group') {
|
||||
if (
|
||||
self::isGroupId(substr($key, strlen(self::GROUP_PREFIX)), $groupIds)
|
||||
self::isGroupId($id, $groupIds)
|
||||
&& is_string($value)
|
||||
&& in_array($value, self::MODES, true)
|
||||
) {
|
||||
$clean[$key] = $value;
|
||||
}
|
||||
} elseif (str_starts_with($key, self::READONLY_PREFIX)) {
|
||||
} elseif ($kind === 'readonly') {
|
||||
if (
|
||||
self::isGroupId(substr($key, strlen(self::READONLY_PREFIX)), $groupIds)
|
||||
self::isGroupId($id, $groupIds)
|
||||
&& ($value === true || $value === '1' || $value === 1)
|
||||
) {
|
||||
$clean[$key] = true;
|
||||
@@ -272,6 +278,28 @@ final class ExposurePolicy
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* What an option key names: "@group:<id>" a group's mode, "@readonly:<id>" a group's
|
||||
* read-only switch, anything else an ability (id = the whole key).
|
||||
*
|
||||
* @return array{kind: 'group'|'readonly'|'tool', id: string}
|
||||
*/
|
||||
private static function parseKey(string $key): array
|
||||
{
|
||||
foreach (['group' => self::GROUP_PREFIX, 'readonly' => self::READONLY_PREFIX] as $kind => $prefix) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
return ['kind' => $kind, 'id' => substr($key, strlen($prefix))];
|
||||
}
|
||||
}
|
||||
|
||||
return ['kind' => 'tool', 'id' => $key];
|
||||
}
|
||||
|
||||
private static function isRequired(string $name): bool
|
||||
{
|
||||
return in_array($name, self::REQUIRED, true);
|
||||
}
|
||||
|
||||
/** @param list<string>|null $groupIds */
|
||||
private static function isGroupId(string $id, ?array $groupIds): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user