Files
heartbeat/docs/USERS.md
T
Andreas Wrede 12e8812070 docs: update notification channel and API docs for form-based management
- NOTIFICATIONS.md: document owner/private fields, channel visibility
  rules, and user-created channels; add troubleshooting note for
  private channel visibility
- HTTP_API.md: add notification channel API endpoints table and full
  endpoint reference (GET types, GET/POST/PUT/DELETE channels)
- USERS.md: add missing PUT /api/0/users/me endpoint documentation
  with all three update modes (identity, channels, password)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 07:45:30 -04:00

8.2 KiB

User Management

Heartbeat supports optional user accounts with role-based access control per host. When no users are configured the server runs in unauthenticated mode — all existing behaviour is unchanged.


Overview

Users are defined in the server config file. Each host can have an owner, zero or more managers, and zero or more monitors. A default owner catches any host that does not name an explicit owner.

Roles

Role Inherits Permissions
monitor View host status, plugin data, alerts; acknowledge alerts they were notified for
manager monitor + Queue commands (/c), trigger DNS re-registration (/n), queue upgrades (/u); add/remove monitors
owner manager + Drop host (/d); add/remove managers; transfer ownership; update host access
admin (flag) owner on all hosts Full access to every host and the user list

admin is a flag on the user, not a per-host role. An admin user has owner-level access on every host without being listed as owner/manager/monitor.


Configuration

Defining users

users:
  andreas:
    full_name: Andreas Wrede
    avatar: /path/to/avatar.png   # file path, URL, or base64 data URI (optional)
    password: pbkdf2:sha256:...   # generated with: hbd passwd andreas
    admin: true                   # optional — grants server-wide owner access

  bob:
    full_name: Bob Smith
    password: pbkdf2:sha256:...
    notification_channels: [pushover_standard]   # channels bob has selected

  carol:
    full_name: Carol Jones
    password: pbkdf2:sha256:...

default_owner: andreas            # owns hosts with no explicit owner
                                  # falls back to the first admin user if omitted

Client-declared host ownership

A host can declare its own owner directly in the hbc or hbc_mini client configuration. This is useful for hosts that are not listed in the server config, or during initial setup before a server-side config entry has been created.

~/.hbc.yaml (hbc):

owner: andreas

~/.hbc.json (hbc_mini):

{ "owner": "andreas" }

When set, the value is included in the os_info plugin data sent to the server. The server applies it as host.owner the first time os_info arrives, provided no owner has been configured server-side for that host. Server-configured ownership always takes precedence.


Assigning roles to hosts

hosts:
  webserver01:
    owner: andreas
    managers: [bob]
    monitors: [carol]
    threshold_config: default
    watch: true
    notification_channels: [pushover_standard]

  unattended-host:              # no owner → owned by default_owner
    threshold_config: default
    watch: true

Generating a password hash

hbd passwd andreas

Enter and confirm the password when prompted. Paste the printed hash into the config file under the user's password key.

You can also generate a hash non-interactively from Python:

from hbd.server.users import hash_password
print(hash_password("mysecret"))

Passwords are stored as PBKDF2-HMAC-SHA256 hashes (260 000 iterations). No third-party libraries are required — only Python's standard hashlib.


Authentication

When at least one user is defined, every request must be authenticated. Unauthenticated requests to HTML pages are redirected to /login; unauthenticated API requests receive 401 Unauthorized.

Browser login

Navigate to any page — you will be redirected to /login automatically. After submitting valid credentials the server sets an hbd_session cookie (HttpOnly, SameSite=Lax, 24 h lifetime). All subsequent requests, including JavaScript fetch() calls on the dashboards, carry the cookie automatically.

To log out, visit /logout.

API / programmatic login

# Log in and capture the token
TOKEN=$(curl -s -X POST http://localhost:50004/api/0/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"andreas","password":"mysecret"}' | jq -r .token)

# Use the token in subsequent requests
curl -H "Authorization: Bearer $TOKEN" http://localhost:50004/api/0/hosts

The token is identical to the session cookie value — both mechanisms work simultaneously.

# Log out
curl -s -X POST http://localhost:50004/api/0/auth/logout \
  -H "Authorization: Bearer $TOKEN"

API Endpoints

Authentication

POST /api/0/auth/login

Obtain a session token.

Request body:

{ "username": "andreas", "password": "mysecret" }

Response:

{ "token": "<opaque-hex-token>", "username": "andreas" }

Also sets the hbd_session cookie for browser clients.

Status codes: 200 OK, 401 Unauthorized, 404 (auth not configured)


POST /api/0/auth/logout

Invalidate the current session.

Headers: Authorization: Bearer <token> or cookie

Response: { "success": true }


Users

GET /api/0/users

List all users. Admin only.

Response:

[
  { "username": "andreas", "full_name": "Andreas Wrede", "avatar": "", "admin": true, "notification_channels": [] },
  { "username": "bob",     "full_name": "Bob Smith",     "avatar": "", "admin": false, "notification_channels": ["pushover_standard"] }
]

GET /api/0/users/me

Return the currently authenticated user's profile.

Response:

{ "username": "carol", "full_name": "Carol Jones", "avatar": "", "admin": false, "notification_channels": [] }

PUT /api/0/users/me

Update the current user's profile. All fields are optional — send only what you want to change.

Update display name and avatar:

{ "full_name": "Carol Jones", "avatar": "/avatars/carol.png" }

Change notification channel selection:

{ "notification_channels": ["pushover_ops", "email_ops"] }

Only channels visible to the user (public + own private) are accepted; others are silently dropped.

Change password:

{ "password": { "current": "oldpass", "new": "newpass" } }

Requires the correct current password. New password is hashed before storage.

Response: {"ok": true}

Status codes: 200 OK, 400 (missing/invalid field), 401 (unauthenticated), 403 (wrong current password)


Host Access

GET /api/0/hosts/{hostname}/access

Return owner/managers/monitors for a host. Requires at least monitor role.

Response:

{
  "owner": "andreas",
  "managers": ["bob"],
  "monitors": ["carol"]
}

PUT /api/0/hosts/{hostname}/access

Update owner/managers/monitors. Requires owner role or admin.

Request body (all fields optional):

{
  "owner": "bob",
  "managers": ["carol"],
  "monitors": []
}

Changes take effect immediately in memory. They are not written back to the config file — reload (SIGHUP) will re-apply config values. To make changes permanent, update the config file.


Host visibility

When users are configured, GET /api/0/hosts only returns hosts the authenticated user has at least monitor access to. Admins see all hosts.


Config reload

On SIGHUP, the server reloads the config file, re-loads the user registry, and re-applies owner/managers/monitors from config to all known hosts. Existing sessions remain valid after a reload.


No-auth mode

If users: is absent or empty, the server starts in unauthenticated mode:

  • No login required — all pages and API endpoints are accessible without credentials.
  • All permission checks pass unconditionally.
  • /login, /logout, and the auth/user API endpoints return 404.

This preserves full backwards compatibility with existing deployments.


Security notes

  • Session tokens are 64-character cryptographically random hex strings (secrets.token_hex(32)).
  • Sessions expire after 24 hours (configurable via users_mod.SESSION_TTL).
  • Cookies are HttpOnly and SameSite=Lax — they are not accessible to JavaScript and are not sent on cross-site requests.
  • The HTTP API does not yet enforce TLS. For production use, place hbd behind a TLS-terminating reverse proxy (nginx, Caddy, etc.) or enable WSS.

See Also