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

Skip to content
openhands edited this page Nov 12, 2026 · 7 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, Active Directory connections and imports
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.

Where a whole controller is stricter than its actions

active_directory_connections and ad_import_batches are Admin-only at the controller level, so reads are gated too. That is a deliberate departure from windows_targets, where reading the list is a Viewer action: the sensitive act there is not saving a row, it is the enumeration. A directory query returns the entire machine inventory of a domain — hostname, OS version, creation date — which is reconnaissance-grade information about the internal network regardless of whether anything is imported afterwards.

The same reasoning sets the nav bar, which is generated from $_models and filtered by each model's hidden? class method. ActiveDirectoryConnection.hidden? shows the tab to Admin only. AdImportBatch.hidden? hides it from everyone: a batch has no index of its own, and the layout links every model in $_models to Model.constantize, so a model with no index route renders a broken tab for every visitor. PollingPriority.hidden? hides itself for a different reason — it does have an index (only: [:index, :edit, :update]), but the bands are reached from the Windows Targets screens, where their effect is visible next to the hosts using them. See Active Directory Import.

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