{% extends 'admin/docs/_layout/layout.twig' %} {% import '_layout/icons.twig' as icons %} {% block title %}Access control{% endblock %} {% block description %}Assign a page or section to a user or group from its sidecar with Lib\Access — public by default, static pages always public.{% endblock %} {% block robots %}noindex, nofollow{% endblock %} {% block docs_content %}

Access control

Lib\Access (novaconium/lib/Access.php) assigns a page to a user, a group, or just "anyone logged in" — from the page's own sidecar, using the accounts {{ icons.lock() }}Admin authentication manages at /admin/users. One call at the top of index.php:

<?php

use Lib\Access;

if ($denied = Access::require('group:members')) {
    return $denied;
}

return [
    // ...normal sidecar context...
];

Access::require() returns null when the request may proceed, or a Response the sidecar returns as-is: nobody logged in → a 303 to /admin/login carrying a ?return= path so a successful login lands right back on the page they wanted; logged in but not allowed → a plain 404, the same hide-don't-tease posture as {{ icons.lock() }}draft pages.

Rules

Admins always pass every rule — the admin role exists to run the site, so there's no way to write a rule that locks an admin out of content.

Public is the default — and static pages are always public

A sidecar that never calls Access::require() is completely untouched by all of this, and a page with no sidecar at all can't call it — so sidecar-less pages are always public. That's load-bearing rather than incidental: only sidecar-less pages are ever written to the {{ icons.book() }}static HTML cache, which Apache serves before PHP (and therefore any access check) runs. Because a gated page necessarily has a sidecar, it's never statically cached, so there's no way to leak a gated page through the cache — the caching/auth rule that drafts and /admin/* need explicit cache exclusions for is satisfied here by construction.

Gating a section

There's no per-directory config for this — a "section" is gated by giving each page in it a sidecar with the same check, which stays visible and greppable at the page level. To keep the rule itself in one place, put a _access.php file in the section directory (any file that isn't index.php/index.twig is invisible to the router) and require it from each sidecar — a PHP file can return a value, so it composes exactly like a direct call:

<?php
// App/pages/members/_access.php — the section's one shared rule
use Lib\Access;

return Access::require('group:members');
<?php
// App/pages/members/anything/index.php — each page in the section
if ($denied = require dirname(__DIR__) . '/_access.php') {
    return $denied;
}

return [];

Interactions worth knowing

{% endblock %}