- Divi-Vollformat: headingLevel nur h1 bis h6 (ValueCleaner, StoredNodeGuard, ShortformWriter); sonst ließ sich für Konten ohne unfiltered_html ein Skript-Element erzeugen. - content-update, divi-update und wp-settings-update sind destructive, damit MCP-Clients nachfragen. - mcp-adapter/execute-ability und get-ability-info nehmen auf dem OAuth-Server nur lit/-Abilities an. - release-publish.sh übergibt den Gitea-Token per Konfigurationsdatei statt als curl-Argument. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
123 lines
3.7 KiB
PHP
123 lines
3.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP;
|
||
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Server\RequestContext;
|
||
use WP_Error;
|
||
use WP_User;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Role gate: only users whose role is allowed get the lit_mcp_use capability.
|
||
*
|
||
* The capability is granted dynamically via user_has_cap, so no role objects
|
||
* in the database are modified.
|
||
*/
|
||
final class Access
|
||
{
|
||
public const CAPABILITY = 'lit_mcp_use';
|
||
public const OPTION = 'lit_mcp_allowed_roles';
|
||
private const DEFAULT_ROLES = ['administrator'];
|
||
private const ADAPTER_META_TOOLS = [
|
||
'discover_abilities',
|
||
'get_ability_info',
|
||
'execute_ability',
|
||
];
|
||
|
||
/** Meta tools of the adapter that name another ability in their input ("ability_name"). */
|
||
private const META_ABILITIES_BY_NAME = ['mcp-adapter/execute-ability', 'mcp-adapter/get-ability-info'];
|
||
|
||
public function register(): void
|
||
{
|
||
add_filter('user_has_cap', [$this, 'grantCapability'], 10, 4);
|
||
add_filter('wp_register_ability_args', [$this, 'limitMetaToolsToOwnAbilities'], 10, 2);
|
||
|
||
foreach (self::ADAPTER_META_TOOLS as $tool) {
|
||
add_filter(
|
||
"mcp_adapter_{$tool}_capability",
|
||
static fn (): string => self::CAPABILITY
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Security audit 2026-11 (B3): the adapter's execute-ability and get-ability-info reach every ability with
|
||
* meta.mcp.public, from any plugin – past the Tools tab ("aus", "nur lesen"), which knows only lit/ tools.
|
||
* On the OAuth-protected server they therefore name lit/ abilities only.
|
||
*
|
||
* @param array<string, mixed> $args
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function limitMetaToolsToOwnAbilities(array $args, string $name): array
|
||
{
|
||
if (! in_array($name, self::META_ABILITIES_BY_NAME, true)) {
|
||
return $args;
|
||
}
|
||
|
||
$inner = $args['permission_callback'] ?? null;
|
||
$args['permission_callback'] = static function (mixed $input = null) use ($inner): bool|WP_Error {
|
||
$target = is_array($input) && is_string($input['ability_name'] ?? null) ? $input['ability_name'] : '';
|
||
if (RequestContext::isOAuthServerRequest() && ! str_starts_with($target, 'lit/')) {
|
||
return Errors::forbidden(
|
||
'Über diesen Server sind nur LIT-MCP-Tools nutzbar.',
|
||
'Nur Abilities mit dem Präfix lit/ ausführen oder abfragen.'
|
||
);
|
||
}
|
||
|
||
return is_callable($inner) ? $inner($input) : false;
|
||
};
|
||
|
||
return $args;
|
||
}
|
||
|
||
/**
|
||
* @return list<string>
|
||
*/
|
||
public function allowedRoles(): array
|
||
{
|
||
$roles = get_option(self::OPTION, self::DEFAULT_ROLES);
|
||
if (! is_array($roles)) {
|
||
$roles = self::DEFAULT_ROLES;
|
||
}
|
||
|
||
$roles = apply_filters('lit_mcp_allowed_roles', $roles);
|
||
if (! is_array($roles)) {
|
||
$roles = self::DEFAULT_ROLES;
|
||
}
|
||
|
||
return array_values(array_filter($roles, 'is_string'));
|
||
}
|
||
|
||
/**
|
||
* @param array<string, bool> $allcaps
|
||
* @param list<string> $caps
|
||
* @param array<int, mixed> $args
|
||
* @return array<string, bool>
|
||
*/
|
||
public function grantCapability(
|
||
array $allcaps,
|
||
array $caps,
|
||
array $args,
|
||
WP_User $user
|
||
): array {
|
||
if (! in_array(self::CAPABILITY, $caps, true)) {
|
||
return $allcaps;
|
||
}
|
||
|
||
if (array_intersect($user->roles, $this->allowedRoles()) !== []) {
|
||
$allcaps[self::CAPABILITY] = true;
|
||
}
|
||
|
||
return $allcaps;
|
||
}
|
||
|
||
public static function currentUserMayUse(): bool
|
||
{
|
||
return current_user_can(self::CAPABILITY);
|
||
}
|
||
}
|