Skip to content

App permissions

4 min read

An app declares the parts of core it intends to use, such as posts, mail or outbound HTTP. The site owner approves those permissions when activating the app. From then on, calls through $this->api() are checked against what was approved. This page explains the model, the catalogue and how refusals behave.

Read this first: it isn't a sandbox

An app is PHP running in the same process as core. Any app could open its own database connection, read .env, or call a core service directly. Nothing in Basehim prevents that, and on shared hosting nothing could without loading apps a completely different way.

What permissions do give you:

  • the site owner can see what an app intends to touch, and approve it;
  • an honest app can't exceed its declaration by accident;
  • overreach shows up in the logs instead of passing silently.

Install apps you have reason to trust.

Enforcement is opt-in

An app that declares no permissions (no permissions key, or an empty list) runs unrestricted, as every app did before permissions existed. The Apps screen badges it as unrestricted.

Declaring a permissions array opts the app in, and from then on the declaration is also its ceiling:

{
  "name": "Feed Importer",
  "slug": "feed-importer",
  "version": "1.0.0",
  "namespace": "Acme\\FeedImporter",
  "permissions": ["posts.write", "terms.write", "http.outbound", "schedule"]
}

Ask for the least you need. Every line is something a site owner has to read and accept.

The catalogue

PermissionRiskAllows
posts.read, pages.readlowReading content, including drafts
posts.write, pages.writemediumCreating and editing content (also custom types, which count as posts)
posts.delete, pages.deletehighTrashing and permanently deleting
media.readlowBrowsing the library
media.writemediumUploading and editing metadata
media.deletehighRemoving files
users.readmediumAccounts, roles and emails (never password hashes)
users.writehighCreating and editing accounts, including roles
users.deletehighRemoving accounts
comments.readlowAll comments, including spam
comments.writemediumCreating comments (skipping the spam guard)
comments.moderatemediumApproving, spamming, trashing
terms.read, terms.writelow, mediumCategories, tags and other taxonomies
menus.read, menus.writelow, mediumNavigation menus
settings.readmediumSite-wide settings
settings.writehighChanging site-wide settings
mail.sendhighSending email from the site's address
http.outboundhighContacting other servers
schedulemediumBackground tasks
db.rawhighRaw SQL through db() and schema(); bypasses everything above

A write implies the matching read. An app with posts.write can read posts.

Always available, never declared: your app's own settings (getSetting/setSetting), its own cache namespace and its own log. Asking a site owner to approve an app's access to its own data would only train people to click through.

Approval

Activating an app that declares permissions opens a consent screen first. The site owner can untick individual permissions. The app then runs with less, and each refusal is logged, so the cause is easy to find.

  • Grants survive deactivation, so switching an app off and on doesn't ask again.
  • Grants are dropped on uninstall.
  • An update that adds a permission doesn't get it automatically. The app keeps running with what was already granted. Calls that need the new permission are refused, and logged, until the site owner approves it on the app's consent screen.
  • An update that adds the first permissions to an app that had none keeps running unrestricted, as before, and is flagged on the Apps screen for review. Enforcement starts once the site owner approves.

What a refusal looks like

API calls degrade instead of throwing. A refused call returns what it would return if there were no data (null, false, 0 or an empty array) and writes a line to your app's log naming the missing permission. A page render never turns into a white screen because of a policy decision.

db() and schema() are the exception: they throw a RuntimeException that names the missing db.raw permission, because every caller expects a real database object.

Check before you try, and degrade gracefully:

if ($this->allowed('mail.send')) {
    $this->api()->mail()->sendTemplate($to, 'Report ready', 'Your report', $html);
} else {
    $this->log('Mail not sent: mail.send was not granted', [], 'warning');
}

Logs

Each app writes to storage/logs/apps/{slug}-YYYY-MM-DD.log, readable under Apps → Logs. Files are kept for 7 days, and a file is rotated at 2 MB. Warnings and errors are also copied to the core log, so a site owner reading the main log sees an app in trouble.

The scanner

When an app is installed, and on demand, Basehim scans its PHP for things that reach around the permission broker:

  • new PDO, eval and shell execution;
  • direct .env reads;
  • disabled TLS verification;
  • a few other patterns.

It's a heuristic, not a verdict. Findings never block an install, but they are shown to the site owner. If your app does one of these things for a good reason, say why in your description.

Choosing permissions for common apps

AppPermissions
Analytics that adds a script and stores hits in its own tabledb.raw
Importer that creates draft posts from a feedposts.write, terms.write, http.outbound, schedule
Weekly digest emailposts.read, users.read, mail.send, schedule
Comment moderation helpercomments.moderate
Settings-only app with an admin screenNothing to declare: its own settings need no permission. Without a permissions list the app is shown as unrestricted

Keep building

More guides, or jump straight into the API reference.

Discussion

Leave a comment