Skip to main content

Roles & assignment

Log Lens has no login of its own: standalone is gated by one shared API token (or nothing, for local use), and the Laravel embed sits entirely behind the host app's own auth. On top of that, an identity and authorization seam (C-2) lets actions be attributed to a person and gated by role — without Log Lens ever owning a user table.

Roles

Three roles, checked against an ability (issues.write, tags.write, connectors.write, releases.write, linear.write, plugins.manage, settings.write, applications.write, or a *.read):

RoleCan do
ownerEverything.
editorEverything except administration (plugins.*, settings.*, applications.*) — but reads of those are allowed.
viewerRead-only — abilities ending in .read; every mutation is rejected with 403.

Any other role name grants nothing, so a typo in a role mapping fails closed.

Gated today, split by tier:

  • Operational (editor + owner): issue status changes, bulk issue actions, issue assignment, tag create/update/delete, connector test/sync/preview/import, recording a deploy, source-map upload/delete, triggering a Linear sync.
  • Administration (owner only — plugins.*/settings.*/applications.*): creating an application (it provisions a whole new tenant: its own database and directories), plugin enable/disable, connector create/update/delete (stores SSH host/credentials), reindex, delete-logs, processed-archive prune, the ingested-severity allowlist, alert channel/rule create/update/delete/test (a channel carries an outbound webhook URL), the release-tracking code-host link settings, Linear settings (API key/webhook secret) and connection test, and rotating the HTTP ingest key.

Reads (the dashboard, the JSON API's GET endpoints) are never gated — only mutations are, with one exception: ?api=applications requires applications.read, because it is the one route dispatched before an application is resolved and so cannot inherit any per-application decision.

Where identity comes from

DeploymentIdentityRoles
StandaloneA single implicit "local owner" (whoever holds the shared token).Always owner.
LaravelThe authenticated host user (auth()->user()), or a log-lens.identity config callable.log-lens.default-role (owner out of the box); the callable can return any of the three.

default-role is the one to change if your access gate admits a broad audience. owner means the gate is the access control — everyone who passes it can change settings, manage plugins, read connector credentials, provision applications, and delete data. Narrow it to editor or viewer and hand out owner deliberately through identity:

LOG_LENS_DEFAULT_ROLE=viewer

A log-lens.identity callable returning null means "no opinion — use the default". Returning anything else without a non-empty id is a misconfiguration and raises an exception rather than quietly falling back to the default role.

Identity is always resolved server-side from the transport (the Laravel adapter, or standalone's implicit owner) — never from a client-supplied header, so it cannot be spoofed.

Per-application roles (Laravel)

log-lens.identity receives the request and the current application id, so a host can return a different role per application — the callable does its own lookup however it likes:

// config/log-lens.php
'identity' => function ($request, $applicationId) {
$user = $request->user();
if (!$user) return null;

// Example with spatie/laravel-permission — but Log Lens has no opinion
// on which package you use. Bouncer, native Gates, or your own roles
// table all work exactly the same way: just return the right string.
$role = $user->hasRole("log-lens-owner-{$applicationId}") ? 'owner'
: ($user->can('edit-logs') ? 'editor' : 'viewer');

return ['id' => $user->id, 'label' => $user->name, 'roles' => [$role]];
},

Log Lens deliberately does not bundle a specific authorization package — the callable is the entire integration surface, and it works identically whichever one (if any) the host app already uses.

Attribution

Mutations record who made them:

  • Issue status changes — every change appends an issue_status_history row with the acting user's label (null = system/unattributed, e.g. an import-seeded change).
  • Tag create/updatetags.created_by/updated_by store the acting user's label the same way. Tags have no per-change history table, so only the most recent creator/editor is tracked, not a full audit trail.

Issue assignment (Laravel-hosted)

Log Lens has no user table, so "assign an issue" means picking from the host's own users. This only works inside a Laravel host app — standalone has no equivalent, since there is nobody to assign to beyond the single local owner.

Configure log-lens.assignable-users to supply the list, scoped to the current application:

// config/log-lens.php
'assignable-users' => function ($request, $applicationId) {
return User::query()
->whereHas('teams', fn ($q) => $q->where('app_id', $applicationId))
->get(['id', 'name'])
->map(fn ($u) => ['id' => (string) $u->id, 'label' => $u->name])
->all();
},

Without this callable, the dashboard's assignee picker has nobody to offer (empty, not an error). See ?api=assign-issue / ?api=assignable-users for the API shape — the stored assigned_to_id/assigned_to_label are an opaque id/label pair from whatever this callable returned, not a foreign key Log Lens resolves itself. Distinct from assignee, a read-only mirror of a Linear issue's assignee (Linear plugin), never written by Log Lens itself.