Repository navigation
Access Control
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.
| 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 |
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.
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 the same reason. 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.
Two roles are seeded by db/seeds.rb:
-
Administrators —
Adminonly. -
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.

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_BASEdefeats CSRF protection (it signs the Railscookie_storesession, which carries only the CSRF token and flash) but does not impersonate anyone. - Rotating
SECRET_KEY_BASElogs nobody out, because sessions are rows looked up by token, not payloads signed with the key. Only the_Events_sessioncookie 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/updateare not routed: minting a session row by hand would bypass the password check entirely. -
session_hashcarries a unique index.gatekeeperresolves 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 hourlyprune_sessionsjob. 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_logondeletes the signing-in user's other rows first, so a second logon reclaims that account's abandoned sessions immediately. -
usernamecarries a unique index and is validated as present and unique.do_logonresolves credentials withfind_by, so before that a second account sharing a name could never sign in, silently, and a blank username was a reachable account.
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.
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.
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).



Screenshots are from a seeded demonstration dataset; see Interface.