diff --git a/src/Access.php b/src/Access.php new file mode 100644 index 0000000..1b22204 --- /dev/null +++ b/src/Access.php @@ -0,0 +1,80 @@ + self::CAPABILITY + ); + } + } + + /** + * @return list + */ + 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); + + return array_values(array_filter($roles, 'is_string')); + } + + /** + * @param array $allcaps + * @param list $caps + * @param array $args + * @return array + */ + 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); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 9484ad7..db2ec0e 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -20,6 +20,8 @@ final class Plugin self::noticeMissing($missing); return; } + + (new Access())->register(); } public static function pluginDir(): string diff --git a/tests/Unit/AccessTest.php b/tests/Unit/AccessTest.php new file mode 100644 index 0000000..c7868f1 --- /dev/null +++ b/tests/Unit/AccessTest.php @@ -0,0 +1,86 @@ +once() + ->with(Access::OPTION, ['administrator']) + ->andReturn(['administrator']); + + self::assertSame(['administrator'], (new Access())->allowedRoles()); + } + + public function testRoleListIsFilterable(): void + { + Functions\when('get_option')->justReturn(['administrator']); + Filters\expectApplied('lit_mcp_allowed_roles') + ->once() + ->andReturn(['administrator', 'editor']); + + self::assertSame(['administrator', 'editor'], (new Access())->allowedRoles()); + } + + public function testGrantsCapabilityToAllowedRole(): void + { + Functions\when('get_option')->justReturn(['editor']); + + $caps = (new Access())->grantCapability( + [], + [Access::CAPABILITY], + [], + new WP_User(5, ['editor']) + ); + + self::assertTrue($caps[Access::CAPABILITY]); + } + + public function testDoesNotGrantCapabilityToOtherRoles(): void + { + Functions\when('get_option')->justReturn(['administrator']); + + $caps = (new Access())->grantCapability( + [], + [Access::CAPABILITY], + [], + new WP_User(7, ['subscriber']) + ); + + self::assertArrayNotHasKey(Access::CAPABILITY, $caps); + } + + public function testCorruptOptionFallsBackToAdministrator(): void + { + Functions\when('get_option')->justReturn('not-an-array'); + + self::assertSame(['administrator'], (new Access())->allowedRoles()); + } + + public function testRegisterRoutesAdapterMetaToolCapabilitiesToOwnCapability(): void + { + // Brain Monkey records add_filter() but never runs callbacks, so we + // capture each registered callback and invoke it ourselves. + foreach (['discover_abilities', 'get_ability_info', 'execute_ability'] as $tool) { + Filters\expectAdded("mcp_adapter_{$tool}_capability") + ->once() + ->whenHappen(static function (callable $callback): void { + self::assertSame(Access::CAPABILITY, $callback('read')); + }); + } + + $access = new Access(); + $access->register(); + + self::assertNotFalse(has_filter('user_has_cap', [$access, 'grantCapability'])); + } +}