feat(demo): add story 1 — Sorano: Rock and Time

This commit is contained in:
2026-06-20 21:19:57 +02:00
parent 42ed59a6b3
commit 8f87155c1d
5508 changed files with 1595740 additions and 124 deletions
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\Api;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Acl\Permissions;
/**
* Hierarchical permission resolver for the API layer.
*
* Grav's User::authorize() requires admin context, so the API uses direct
* access-array lookups. This class adds parent-key inheritance so granting
* "api.pages" implicitly covers "api.pages.read", matching how Grav's
* core Access::get() resolves permissions.
*/
class PermissionResolver
{
/** @var array<string, mixed>|null Lazy-flattened user access map (one per instance). */
private ?array $flatAccess = null;
/** @var UserInterface|null The user whose access was flattened — used to invalidate cache. */
private ?UserInterface $flatAccessUser = null;
public function __construct(private readonly Permissions $permissions) {}
/**
* Resolve a single permission for a user with parent-key inheritance.
*
* Walks up the dot-path (api.pages.read → api.pages → api) and returns
* the first explicitly set value, or null if nothing is set at any level.
*/
public function resolve(UserInterface $user, string $permission): ?bool
{
$flat = $this->getFlatAccess($user);
$key = $permission;
while ($key !== '') {
if (array_key_exists($key, $flat)) {
$value = $flat[$key];
if (is_bool($value)) {
return $value;
}
if ($value === 1 || $value === '1' || $value === 'true') {
return true;
}
if ($value === 0 || $value === '0' || $value === 'false' || $value === null) {
return false;
}
}
$pos = strrpos($key, '.');
$key = $pos !== false ? substr($key, 0, $pos) : '';
}
return null;
}
/**
* Build a flat map of all registered api.* permissions with resolved
* true/false values. Super-admins receive true for everything.
*
* @return array<string, bool>
*/
public function resolvedMap(UserInterface $user, bool $isSuperAdmin): array
{
$allInstances = $this->permissions->getInstances();
$result = [];
foreach ($allInstances as $name => $action) {
if (!str_starts_with($name, 'api.')) {
continue;
}
$result[$name] = $isSuperAdmin ? true : (bool) $this->resolve($user, $name);
}
return $result;
}
/**
* Lazily flatten $user->get('access') from nested array to dot-notation keys.
* Cached per user instance within this resolver.
*/
private function getFlatAccess(UserInterface $user): array
{
if ($this->flatAccess === null || $this->flatAccessUser !== $user) {
$nested = $user->get('access');
$this->flatAccess = is_array($nested)
? Utils::arrayFlattenDotNotation($nested)
: [];
$this->flatAccessUser = $user;
}
return $this->flatAccess;
}
}