> For the complete documentation index, see [llms.txt](https://playground.e107sk.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://playground.e107sk.com/plugin-dev-quick-notes/admin-ui/perm-inside-usdadminmenu.md).

# 'perm'\` inside \`$adminMenu\`

## Permissions: `$adminMenu` does not gate anything

**The trap.** Setting `'perm'` inside `$adminMenu` feels like it restricts the route. It does not. It only hides the menu link.

```php
// hides the link. does NOT block the route.
protected $adminMenu = array(
    'main/list'   => array('caption' => LAN_MANAGE, 'perm' => 'P'),
    'main/create' => array('caption' => LAN_CREATE, 'perm' => '0'),
);
```

A user with `P` rights will not see the "Create" link — and can still reach `admin_config.php?mode=main&action=create` by typing the URL. **Hiding a link is not authorisation.**

#### Where the gate actually is

`$adminMenu` is read in exactly four places in `admin_ui.php`: resolving the default mode, the getter/setter, `renderMenu()`, and `restrictMenuAccess()`. It never appears in `checkAccess()`, `hasModeAccess()` or `hasRouteAccess()`.

The real gate is the dispatcher's **`$perm`** property:

```php
protected $perm = '0';                    // string: gates the whole dispatcher
                                          //   -> hasModeAccess()

protected $perm = array(                  // array: gates per route
    'main/prefs' => '0',                  //   -> hasRouteAccess()
);
```

The core docblock above `$adminMenu` confirms it: `'perm'` and `'userclass'` restrictions are *inherited* from `$modes`, `$access` and `$perm` — you do not need to set them in the menu. And because `restrictMenuAccess()` itself calls `hasRouteAccess()`, setting `$perm` also hides the links. So `$perm` alone does both jobs; `'perm'` keys in `$adminMenu` are redundant.

#### Which route keys are real

Only these are actual mode/action routes and can be gated by a `$perm` array:

| Key           | Method          |
| ------------- | --------------- |
| `main/list`   | `ListPage()`    |
| `main/create` | `CreatePage()`  |
| `main/edit`   | `EditPage()`    |
| `main/prefs`  | `PrefsPage()`   |
| `main/inline` | `inline` action |

These are **not** routes — they are triggers that POST to `main/list`:

| Key           | Reality               |
| ------------- | --------------------- |
| `main/delete` | `ListDeleteTrigger()` |
| `main/batch`  | `ListBatchTrigger()`  |
| `main/copy`   | a batch trigger       |

Putting `'main/delete' => '0'` in `$perm` is **dead configuration**. It never matches a route. Worse, it reads as if delete is protected when it is not: delete, copy and batch all run under whatever permission `main/list` has.

**Consequence.** A clean read/write split (list at `P`, writing at `0`) is not achievable through `$perm` alone — you would have to override `ListDeleteTrigger()` and `ListBatchTrigger()`. For a small plugin it is usually better to gate the whole thing:

```php
// admin_config.php
if(!getperms('0'))
{
    e107::redirect('admin');
    exit;
}
```

```php
// dispatcher
protected $perm = '0';
```

***
