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 |
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.
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.