Skip to content

Commit 7a341f1

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 d02d93e commit 7a341f1

File tree

7 files changed

+271
-9
lines changed

7 files changed

+271
-9
lines changed

src/ACL/AccessControlManager.php

Lines changed: 84 additions & 0 deletions
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

Lines changed: 25 additions & 0 deletions
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

Lines changed: 29 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 30 additions & 9 deletions
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;
@@ -336,7 +338,7 @@ public function get_data( ?DataSource $data_source = null, ?Pagination $paginati
336338
*/
337339
$data = $data_source->get_data_by_id( $data_id );
338340

339-
foreach ( $this->directory_fields as $field ) {
341+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
340342
$data[ $field->uuid() ] = $field->get_value( $data );
341343
}
342344

@@ -359,31 +361,32 @@ public function get_data( ?DataSource $data_source = null, ?Pagination $paginati
359361
* @throws DataSourceException When the data source encounters an issue.
360362
*/
361363
public function get_view_data_item( string $data_id ): DataItem {
362-
$data = $this->data_source()->get_data_by_id( $data_id );
364+
$data = $this->data_source()->get_data_by_id( $data_id );
365+
$fields = $this->allowed_fields( $this->view_fields );
363366

364-
foreach ( $this->view_fields as $field ) {
367+
foreach ( $fields as $field ) {
365368
$data[ $field->uuid() ] = $field->get_value( $data );
366369
}
367370

368371
return DataItem::from_array(
369372
[
370-
'fields' => $this->view_fields,
373+
'fields' => $fields,
371374
'data' => $data,
372375
],
373376
);
374377
}
375378

376379
/**
377-
* Returns all the fields for the dictionary view.
380+
* Returns all the fields for the directory view.
378381
*
379382
* @since $ver$
380383
*
381384
* @return array[] The fields as arrays.
382385
*/
383-
private function dictionary_fields(): array {
386+
private function directory_fields_for_json(): array {
384387
$fields = [];
385388

386-
foreach ( $this->directory_fields as $field ) {
389+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
387390
$fields[] = array_filter(
388391
$field->to_array(),
389392
static fn( $value ) => ! is_null( $value ),
@@ -426,7 +429,7 @@ private function default_layouts(): array {
426429
private function get_field_ids( ?callable $filter = null ): array {
427430
$hidden_fields = [];
428431

429-
foreach ( $this->directory_fields as $field ) {
432+
foreach ( $this->allowed_fields( $this->directory_fields ) as $field ) {
430433
if ( $filter && ! $filter( $field ) ) {
431434
continue;
432435
}
@@ -520,7 +523,7 @@ public function to_array(): array {
520523
'defaultLayouts' => $this->default_layouts(),
521524
'paginationInfo' => $this->pagination->info( $this->data_source() ),
522525
'view' => $this->view(),
523-
'fields' => $this->dictionary_fields(),
526+
'fields' => $this->directory_fields_for_json(),
524527
'data' => $this->get_data(),
525528
'actions' => $this->actions ? $this->actions->to_array() : [],
526529
];
@@ -660,4 +663,22 @@ private function layout( View $view ): array {
660663

661664
return array_filter( $output );
662665
}
666+
667+
/**
668+
* Filters out the fields the current user cannot view.
669+
*
670+
* @since $ver$
671+
*
672+
* @return Field[] The fields.
673+
*/
674+
private function allowed_fields( array $fields ): array {
675+
return array_filter(
676+
$fields,
677+
fn( Field $field ) => AccessControlManager::current()->can(
678+
Capability::view_dataview_field(),
679+
$this,
680+
$field
681+
)
682+
);
683+
}
663684
}
Lines changed: 57 additions & 0 deletions
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+
}
Lines changed: 27 additions & 0 deletions
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)