Skip to content

Commit 9712b9f

Browse files
zackkatzdoekenorg
authored andcommitted
Add ACL Management
- Add Access Controller interface - Add Access Controller Manager with tests - Add Capability enum - Add All Access Controller implementation
1 parent 67da62a commit 9712b9f

7 files changed

+271
-9
lines changed

src/ACL/AccessControlManager.php

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\ACL;
4+
5+
use SplStack;
6+
7+
/**
8+
* Manages the current and previously set Access Controllers.
9+
*
10+
* By default, the manager will return a {@see ReadOnlyAccessController}.
11+
*
12+
* @since $ver$
13+
*/
14+
final class AccessControlManager {
15+
/**
16+
* The current stack of Access Control Managers.
17+
*
18+
* @since $ver$
19+
*
20+
* @var SplStack<AccessController>
21+
*/
22+
private static \SplStack $access_controllers;
23+
24+
/**
25+
* Prevent creating multiple instances of the manager.
26+
*
27+
* @since $ver$
28+
*/
29+
private function __construct() {
30+
}
31+
32+
/**
33+
* Lazily initializes the Access Control Manager.
34+
*
35+
* @since $ver$
36+
*
37+
* @return void
38+
*/
39+
private static function initialize(): void {
40+
if ( ! isset( self::$access_controllers ) ) {
41+
self::$access_controllers = new \SplStack();
42+
self::set( new ReadOnlyAccessController() );
43+
}
44+
}
45+
46+
/**
47+
* Sets the current AccessController.
48+
*
49+
* @since $ver$
50+
*
51+
* @param AccessController $access_controller The access controller.
52+
*/
53+
public static function set( AccessController $access_controller ): void {
54+
self::initialize();
55+
56+
self::$access_controllers->push( $access_controller );
57+
}
58+
59+
/**
60+
* Returns the current Access Controller.
61+
*
62+
* @since $ver$
63+
*
64+
* @return AccessController The current AccessController.
65+
*/
66+
public static function current(): AccessController {
67+
self::initialize();
68+
69+
return self::$access_controllers->top();
70+
}
71+
72+
/**
73+
* Resets the current Access Controller to the previous.
74+
*
75+
* @since $ver$
76+
*/
77+
public static function reset(): void {
78+
self::initialize();
79+
80+
if ( self::$access_controllers->count() > 1 ) {
81+
self::$access_controllers->pop();
82+
}
83+
}
84+
}

src/ACL/AccessController.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\ACL;
4+
5+
use DataKit\DataViews\DataView\DataView;
6+
use DataKit\DataViews\Field\Field;
7+
8+
/**
9+
* Manages the access the current user has on {@see DataView} and {@see Field} objects.
10+
*
11+
* @since $ver$
12+
*/
13+
interface AccessController {
14+
/**
15+
* Returns whether the user has the provided capability.
16+
*
17+
* @since $ver$
18+
*
19+
* @param Capability $capability The capability to test.
20+
* @param mixed ...$context The available context for the test.
21+
*
22+
* @return bool Whether the user has the capability.
23+
*/
24+
public function can( Capability $capability, ...$context ): bool;
25+
}

src/ACL/Capability.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\ACL;
4+
5+
use DataKit\DataViews\EnumObject;
6+
7+
/**
8+
* Represents a capability a user can have.
9+
*
10+
* @since $ver$
11+
*
12+
* @method static self view_dataview() Whether the user can view a DataView.
13+
* @method static self edit_dataview() Whether the user can edit a DataView.
14+
* @method static self view_dataview_field() Whether the user can view a DataView field.
15+
*/
16+
final class Capability extends EnumObject {
17+
/**
18+
* {@inheritDoc}
19+
*
20+
* @since $ver$
21+
*/
22+
protected static function cases(): array {
23+
return [
24+
'view_dataview' => 'view_dataview',
25+
'edit_dataview' => 'edit_dataview',
26+
'view_dataview_field' => 'view_dataview_field',
27+
];
28+
}
29+
}

src/ACL/ReadOnlyAccessController.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\ACL;
4+
5+
/**
6+
* AccessControlManager that allows full access to anyone.
7+
*
8+
* @since $ver$
9+
*/
10+
final class ReadOnlyAccessController implements AccessController {
11+
/**
12+
* {@inheritDoc}
13+
*
14+
* @since $ver$
15+
*/
16+
public function can( Capability $capability, ...$context ): bool {
17+
return strpos( $capability->as_string(), 'view_' ) === 0;
18+
}
19+
}

src/DataView/DataView.php

+30-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace DataKit\DataViews\DataView;
44

5+
use DataKit\DataViews\ACL\AccessControlManager;
6+
use DataKit\DataViews\ACL\Capability;
57
use DataKit\DataViews\Data\DataSource;
68
use DataKit\DataViews\Data\Exception\DataSourceException;
79
use DataKit\DataViews\Data\MutableDataSource;
@@ -354,7 +356,7 @@ public function get_data( ?DataSource $data_source = null, ?Pagination $paginati
354356
*/
355357
$data = $data_source->get_data_by_id( $data_id );
356358

357-
foreach ( $this->directory_fields as $field ) {
359+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
358360
$data[ $field->uuid() ] = $field->get_value( $data );
359361
}
360362

@@ -377,31 +379,32 @@ public function get_data( ?DataSource $data_source = null, ?Pagination $paginati
377379
* @throws DataSourceException When the data source encounters an issue.
378380
*/
379381
public function get_view_data_item( string $data_id ): DataItem {
380-
$data = $this->data_source()->get_data_by_id( $data_id );
382+
$data = $this->data_source()->get_data_by_id( $data_id );
383+
$fields = $this->allowed_fields( $this->view_fields );
381384

382-
foreach ( $this->view_fields as $field ) {
385+
foreach ( $fields as $field ) {
383386
$data[ $field->uuid() ] = $field->get_value( $data );
384387
}
385388

386389
return DataItem::from_array(
387390
[
388-
'fields' => $this->view_fields,
391+
'fields' => $fields,
389392
'data' => $data,
390393
],
391394
);
392395
}
393396

394397
/**
395-
* Returns all the fields for the dictionary view.
398+
* Returns all the fields for the directory view.
396399
*
397400
* @since $ver$
398401
*
399402
* @return array[] The fields as arrays.
400403
*/
401-
private function dictionary_fields(): array {
404+
private function directory_fields_for_json(): array {
402405
$fields = [];
403406

404-
foreach ( $this->directory_fields as $field ) {
407+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
405408
$fields[] = array_filter(
406409
$field->to_array(),
407410
static fn( $value ) => ! is_null( $value ),
@@ -448,7 +451,7 @@ private function default_layouts(): array {
448451
private function get_field_ids( ?callable $filter = null ): array {
449452
$field_ids = [];
450453

451-
foreach ( $this->directory_fields as $field ) {
454+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
452455
if ( $filter && ! $filter( $field ) ) {
453456
continue;
454457
}
@@ -542,7 +545,7 @@ public function to_array(): array {
542545
'defaultLayouts' => $this->default_layouts(),
543546
'paginationInfo' => $this->pagination->info( $this->data_source() ),
544547
'view' => $this->view(),
545-
'fields' => $this->dictionary_fields(),
548+
'fields' => $this->directory_fields_for_json(),
546549
'data' => $this->get_data(),
547550
'actions' => $this->actions ? $this->actions->to_array() : [],
548551
];
@@ -734,4 +737,22 @@ private function get_media_field_id(): string {
734737

735738
return $image_fields ? reset( $image_fields ) : '';
736739
}
740+
741+
/**
742+
* Filters out the fields the current user cannot view.
743+
*
744+
* @since $ver$
745+
*
746+
* @return Field[] The fields.
747+
*/
748+
private function allowed_fields( array $fields ): array {
749+
return array_filter(
750+
$fields,
751+
fn( Field $field ) => AccessControlManager::current()->can(
752+
Capability::view_dataview_field(),
753+
$this,
754+
$field
755+
)
756+
);
757+
}
737758
}
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\Tests\ACL;
4+
5+
use DataKit\DataViews\ACL\AccessController;
6+
use DataKit\DataViews\ACL\AccessControlManager;
7+
use DataKit\DataViews\ACL\Capability;
8+
use DataKit\DataViews\ACL\ReadOnlyAccessController;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* Unit tests for {@see AccessControlManager}
13+
*
14+
* @since $ver$
15+
*/
16+
final class AccessControlManagerTest extends TestCase {
17+
/**
18+
* Creates an in-memory access controller instance.
19+
*
20+
* @since $ver$
21+
*
22+
* @return AccessController
23+
*/
24+
private static function create_access_controller(): AccessController {
25+
return new class implements AccessController {
26+
public function can( Capability $capability, ...$context ): bool {
27+
return false;
28+
}
29+
};
30+
}
31+
32+
/**
33+
* Test case for
34+
*
35+
* @since $ver$
36+
*/
37+
public function test_manager(): void {
38+
self::assertInstanceOf( ReadOnlyAccessController::class, AccessControlManager::current() );
39+
40+
$fake = self::create_access_controller();
41+
$fake_2 = self::create_access_controller();
42+
43+
AccessControlManager::set( $fake );
44+
AccessControlManager::set( $fake_2 );
45+
46+
self::assertNotInstanceOf( ReadOnlyAccessController::class, AccessControlManager::current() );
47+
self::assertSame( $fake_2, AccessControlManager::current() );
48+
49+
AccessControlManager::reset();
50+
self::assertSame( $fake, AccessControlManager::current() );
51+
52+
AccessControlManager::reset();
53+
AccessControlManager::reset(); // Can't reset beyond all access.
54+
55+
self::assertInstanceOf( ReadOnlyAccessController::class, AccessControlManager::current() );
56+
}
57+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\Tests\ACL;
4+
5+
use DataKit\DataViews\ACL\Capability;
6+
use DataKit\DataViews\ACL\ReadOnlyAccessController;
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* Unit tests for {@see ReadOnlyAccessController}
11+
*
12+
* @since $ver$
13+
*/
14+
final class ReadOnlyAccessControllerTest extends TestCase {
15+
/**
16+
* Test case for
17+
*
18+
* @since $ver$
19+
*/
20+
public function test_controller(): void {
21+
$controller = new ReadOnlyAccessController();
22+
23+
self::assertTrue( $controller->can( Capability::view_dataview() ) );
24+
self::assertTrue( $controller->can( Capability::view_dataview_field() ) );
25+
self::assertFalse( $controller->can( Capability::edit_dataview() ) );
26+
}
27+
}

0 commit comments

Comments
 (0)