8000 Access Control · dhoelzer/DAD Wiki · GitHub
[go: up one dir, main page]

Skip to content
openhands edited this page Nov 11, 2026 · 5 revisions

Authorization is one fail-closed filter. ApplicationController::CONTROLLER_RIGHTS maps each controller to the rights that may reach it, and a controller missing from that map is unreachable to everyone — denying by default rather than allowing by default.

Rights

right controllers it unlocks
Admin everything, including users, roles, rights, sessions, preferences, services, statistics, words, hunks
Viewer events, alerts, searches, systems, Windows targets and priority bands (read only)
Commentator commenting on alerts, and exclusions
Taskmaster the jobs screens
Detective displays and stored searches

Per-action overrides

ACTION_RIGHTS tightens specific actions past their controller's default, because a controller-level right is the wrong granularity when some actions carry a different kind of risk.

controller action requires
windows_targets new, create, edit, update, poll_now Admin
polling_priorities edit, update Admin
searches new, create, edit, update, destroy Detective

A Windows target row holds a domain credential and names a host this server will connect out to, and poll_now uses that credential to make the connection — so reading one is a Viewer action but writing or polling one is not. Widening a priority band loosens how often DAD looks for new events, which is likewise an administrative change. A Viewer can see both lists and the guarantee each band offers, but cannot change either.

ApplicationController.required_rights is a class method, so the policy is assertable without instantiating a request — which matters for actions whose failure and refusal produce the same response.

Roles

Two roles are seeded by db/seeds.rb:

  • Administrators — Admin only.
  • Analyst — Viewer, Commentator, Taskmaster, Detective.

Seeding is idempotent: every lookup is find_or_create_by! and every join is guarded, so re-running it leaves the same counts. It deliberately does not reset an admin password that has been changed since.

The admin password comes from DAD_ADMIN_PASSWORD; seeding raises rather than creating a well-known default account.

Log on

Login state is a database row, not a signed cookie

gatekeeper reads cookies[:sessionID] and resolves it with Session.find_by(session_hash: …). The token is SecureRandom.hex(32) and is written as a plain cookie value. Nothing in app/, lib/ or config/ uses cookies.signed, cookies.encrypted, MessageVerifier, or MessageEncryptor.

Consequences worth remembering:

  • Leaking SECRET_KEY_BASE defeats CSRF protection (it signs the Rails cookie_store session, which carries only the CSRF token and flash) but does not impersonate anyone.
  • Rotating SECRET_KEY_BASE logs nobody out, because sessions are rows looked up by token, not payloads signed with the key. Only the _Events_session cookie is invalidated, and it is re-minted on the next request.
  • Sessions expire after 1 hour and are refreshed to +1 hour on each request. An admin can force-terminate one via sessions#destroy.
  • sessions#create/new/edit/update are not routed: minting a session row by hand would bypass the password check entirely.
  • session_hash carries a unique index. gatekeeper resolves the cookie on every authenticated request, and that lookup used to be a full table scan.
  • Expired rows are reclaimed by Session.purge_expired, called on logon and by the seeded hourly prune_sessions job. Without that, a browser that was closed never presents its token again, so its row — and the table the per-request lookup reads — grew without bound.
  • do_logon deletes the signing-in user's other rows first, so a second logon reclaims that account's abandoned sessions immediately.
  • username carries a unique index and is validated as present and unique. do_logon resolves credentials with find_by, so before that a second account sharing a name could never sign in, silently, and a blank username was a reachable account.

Lockout

Logon increments attempts on a bad password and locks the account past 4 attempts. The counter resets 5 minutes after the last attempt, and on success.

Password storage

Passwords are bcrypt (BCrypt::Password.create, explicit cost). A legacy salt$sha256hex hash still verifies and is transparently upgraded to bcrypt on the next successful logon. Passwords longer than 72 bytes are rejected rather than silently truncated by bcrypt.

The stored hash is never rendered into HTML or the JSON API. The edit form does not pre-fill the password field — resubmitting it used to re-hash the hash and lock the account out.

Changing a user's rights

Rights are granted through roles: users#role_add / role_remove, and roles#right_add / right_remove. Both are POST and both are Admin-only. A reloaded User reports fresh rights (User#right_names is invalidated on reload).

Users

Rights held by a role

Roles held by a user

Screenshots are from a seeded demonstration dataset; see Interface.

Clone this wiki locally

0