fix: library entries read as not found without the library right (P12)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,9 @@ class LibraryAccess
|
|||||||
/**
|
/**
|
||||||
* The library entry $postId after the checks for reading, or their refusal; null when $postId
|
* The library entry $postId after the checks for reading, or their refusal; null when $postId
|
||||||
* names no library entry (the caller goes on with PostGuard). With the library hidden (ruling
|
* names no library entry (the caller goes on with PostGuard). With the library hidden (ruling
|
||||||
* P9), an entry is refused exactly like an ID that does not exist – before any rights check.
|
* P9) or without the library right at all (ruling P12), an entry is refused exactly like an ID
|
||||||
|
* that does not exist, so its type and existence stay unknown; only a user with the library
|
||||||
|
* right who lacks edit_post on the entry gets forbidden (like pages).
|
||||||
*/
|
*/
|
||||||
public function item(mixed $postId, bool $forWrite): WP_Post|WP_Error|null
|
public function item(mixed $postId, bool $forWrite): WP_Post|WP_Error|null
|
||||||
{
|
{
|
||||||
@@ -61,7 +63,7 @@ class LibraryAccess
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->visible !== null && ! ($this->visible)()) {
|
if (($this->visible !== null && ! ($this->visible)()) || $this->canList() !== null) {
|
||||||
return PostGuard::notFound((int) $post->ID);
|
return PostGuard::notFound((int) $post->ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,10 +66,17 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
$schema->allows('isManaged')->andReturn(false);
|
$schema->allows('isManaged')->andReturn(false);
|
||||||
$outline = static fn (DiviIntegration $divi): mixed => $divi->abilities()[0]->permission(['post_id' => 1055]);
|
$outline = static fn (DiviIntegration $divi): mixed => $divi->abilities()[0]->permission(['post_id' => 1055]);
|
||||||
|
|
||||||
// No Divi in unit tests: DiviRoles allows nothing, so a reachable entry is refused, not missing.
|
// No Divi in unit tests: DiviRoles allows nothing, so the entry is not found either way (ruling P12);
|
||||||
$visible = new DiviIntegration($schema, FakeChangeLog::forbidden(), null, static fn (): bool => true);
|
// the wiring shows in which gate the integration asks.
|
||||||
self::assertToolError($outline($visible), 'forbidden');
|
$asked = 0;
|
||||||
self::assertToolError($outline(new DiviIntegration($schema, FakeChangeLog::forbidden())), 'forbidden');
|
$visible = new DiviIntegration($schema, FakeChangeLog::forbidden(), null, static function () use (&$asked): bool {
|
||||||
|
++$asked;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
self::assertToolError($outline($visible), 'not_found');
|
||||||
|
self::assertSame(1, $asked);
|
||||||
|
self::assertToolError($outline(new DiviIntegration($schema, FakeChangeLog::forbidden())), 'not_found');
|
||||||
|
|
||||||
$hidden = new DiviIntegration($schema, FakeChangeLog::forbidden(), libraryVisible: static fn (): bool => false);
|
$hidden = new DiviIntegration($schema, FakeChangeLog::forbidden(), libraryVisible: static fn (): bool => false);
|
||||||
$refusal = $outline($hidden);
|
$refusal = $outline($hidden);
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||||
|
|
||||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||||
|
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
use WP_Error;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
|
|
||||||
final class LibraryAccessTest extends TestCase
|
final class LibraryAccessTest extends TestCase
|
||||||
@@ -24,15 +28,18 @@ final class LibraryAccessTest extends TestCase
|
|||||||
self::assertNull($access->canList());
|
self::assertNull($access->canList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling P12: without the library right at all, library IDs read like missing IDs (no type oracle). */
|
||||||
public function testReadingNeedsEditPostsTheDiviLibraryRoleAndEditPost(): void
|
public function testReadingNeedsEditPostsTheDiviLibraryRoleAndEditPost(): void
|
||||||
{
|
{
|
||||||
$item = LibraryFixtures::item(1055);
|
$item = LibraryFixtures::item(1055);
|
||||||
|
|
||||||
LibraryFixtures::given([$item], ['edit_posts']);
|
LibraryFixtures::given([$item], ['edit_posts']);
|
||||||
self::assertToolError(LibraryFixtures::access()->item(1055, false), 'forbidden');
|
self::assertToolError(LibraryFixtures::access()->item(1055, false), 'not_found');
|
||||||
|
self::assertToolError(LibraryFixtures::access()->canList(), 'forbidden');
|
||||||
|
|
||||||
LibraryFixtures::given([$item]);
|
LibraryFixtures::given([$item]);
|
||||||
self::assertToolError(LibraryFixtures::access(['divi_library'])->item(1055, false), 'forbidden');
|
self::assertToolError(LibraryFixtures::access(['divi_library'])->item(1055, false), 'not_found');
|
||||||
|
self::assertToolError(LibraryFixtures::access(['divi_library'])->canList(), 'forbidden');
|
||||||
|
|
||||||
LibraryFixtures::given([$item], ['edit_post']);
|
LibraryFixtures::given([$item], ['edit_post']);
|
||||||
$refusal = LibraryFixtures::access()->item(1055, false);
|
$refusal = LibraryFixtures::access()->item(1055, false);
|
||||||
@@ -63,7 +70,7 @@ final class LibraryAccessTest extends TestCase
|
|||||||
foreach ([['edit_posts'], ['edit_post']] as $denied) {
|
foreach ([['edit_posts'], ['edit_post']] as $denied) {
|
||||||
LibraryFixtures::given([$item], $denied);
|
LibraryFixtures::given([$item], $denied);
|
||||||
$refusal = LibraryFixtures::access()->item(1055, false);
|
$refusal = LibraryFixtures::access()->item(1055, false);
|
||||||
self::assertToolError($refusal, 'forbidden');
|
self::assertToolError($refusal, $denied === ['edit_post'] ? 'forbidden' : 'not_found');
|
||||||
self::assertStringNotContainsString('Geheimer Entwurf', $refusal->get_error_message());
|
self::assertStringNotContainsString('Geheimer Entwurf', $refusal->get_error_message());
|
||||||
self::assertStringNotContainsString('Geheimer Entwurf', (string) wp_json_encode($refusal->get_error_data()));
|
self::assertStringNotContainsString('Geheimer Entwurf', (string) wp_json_encode($refusal->get_error_data()));
|
||||||
}
|
}
|
||||||
@@ -85,4 +92,25 @@ final class LibraryAccessTest extends TestCase
|
|||||||
self::assertNull($hidden->item(LibraryFixtures::PAGE, false), 'pages stay PostGuard\'s business');
|
self::assertNull($hidden->item(LibraryFixtures::PAGE, false), 'pages stay PostGuard\'s business');
|
||||||
self::assertNull($hidden->item(9999, false));
|
self::assertNull($hidden->item(9999, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling P12: the refusal without the library right is byte-identical to a missing ID's. */
|
||||||
|
public function testWithoutTheLibraryRightAnEntryIsIndistinguishableFromAMissingId(): void
|
||||||
|
{
|
||||||
|
$schema = Mockery::mock(ContentSchema::class);
|
||||||
|
$schema->allows('isManaged')->andReturn(false);
|
||||||
|
|
||||||
|
foreach ([[['edit_posts'], []], [[], ['divi_library']]] as [$denied, $deniedRoles]) {
|
||||||
|
LibraryFixtures::given([LibraryFixtures::item(1055, ['post_title' => 'Geheimer Entwurf'])], $denied);
|
||||||
|
$refusal = LibraryFixtures::access($deniedRoles)->item(1055, false);
|
||||||
|
|
||||||
|
LibraryFixtures::given([], $denied);
|
||||||
|
$missing = PostGuard::editable(1055, $schema, 'post_id', true);
|
||||||
|
|
||||||
|
self::assertInstanceOf(WP_Error::class, $refusal);
|
||||||
|
self::assertInstanceOf(WP_Error::class, $missing);
|
||||||
|
self::assertSame($missing->get_error_code(), $refusal->get_error_code());
|
||||||
|
self::assertSame($missing->get_error_message(), $refusal->get_error_message());
|
||||||
|
self::assertSame($missing->get_error_data(), $refusal->get_error_data());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ final class DiviPageLoaderTest extends TestCase
|
|||||||
self::assertToolError((new DiviPageLoader($schema))->load(1058, false), 'not_found');
|
self::assertToolError((new DiviPageLoader($schema))->load(1058, false), 'not_found');
|
||||||
self::assertToolError(
|
self::assertToolError(
|
||||||
(new DiviPageLoader($schema, new Parser(), LibraryFixtures::access(['divi_library'])))->load(1058, false),
|
(new DiviPageLoader($schema, new Parser(), LibraryFixtures::access(['divi_library'])))->load(1058, false),
|
||||||
'forbidden'
|
'not_found'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ final class DiviReadAbilitiesTest extends TestCase
|
|||||||
self::assertSame('https://kunde.test/wp-admin/post.php?post=1059&action=edit', $result['edit_url']);
|
self::assertSame('https://kunde.test/wp-admin/post.php?post=1059&action=edit', $result['edit_url']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNodeGetOfALibraryItemAndRefusalsWithoutTheLibraryRight(): void
|
public function testNodeGetOfALibraryItemAndNotFoundWithoutTheLibraryRight(): void
|
||||||
{
|
{
|
||||||
LibraryFixtures::given([LibraryFixtures::item(1057, ['post_status' => 'private', 'post_title' => 'Geheim'])]);
|
LibraryFixtures::given([LibraryFixtures::item(1057, ['post_status' => 'private', 'post_title' => 'Geheim'])]);
|
||||||
$ability = new DiviNodeGetAbility(LibraryFixtures::loader(), $this->design());
|
$ability = new DiviNodeGetAbility(LibraryFixtures::loader(), $this->design());
|
||||||
@@ -162,9 +162,9 @@ final class DiviReadAbilitiesTest extends TestCase
|
|||||||
|
|
||||||
$denied = new DiviNodeGetAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
$denied = new DiviNodeGetAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
||||||
$refusal = $denied->permission(['post_id' => 1057, 'id' => 'm0']);
|
$refusal = $denied->permission(['post_id' => 1057, 'id' => 'm0']);
|
||||||
self::assertToolError($refusal, 'forbidden');
|
self::assertToolError($refusal, 'not_found');
|
||||||
self::assertStringNotContainsString('Geheim', $refusal->get_error_message());
|
self::assertStringNotContainsString('Geheim', $refusal->get_error_message());
|
||||||
$outline = new DiviOutlineAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
$outline = new DiviOutlineAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
||||||
self::assertToolError($outline->execute(['post_id' => 1057]), 'forbidden');
|
self::assertToolError($outline->execute(['post_id' => 1057]), 'not_found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user