926aa2d3ec
Part of the same security-hardening pass as the last five commits, and
the last item in its backlog. User API keys had no expiry at all --
unlike service-account keys, visibly distinct only by their "tdsa_"
prefix -- and, it turns out while implementing this, no way to list
them either: only create (returns the raw key once) and delete-by-id
existed, so a key's owner had no way to even discover what keys they
had short of remembering IDs from creation time.
handleCreateAPIKey takes an optional expires_in_days (0, the default,
keeps today's behavior: never expires, so no existing integration is
affected). apiKeyUser's lookup now carries `expires_at IS NULL OR
expires_at > now` as part of the query itself, the same way serveAs's
disabled_at check already works -- an expired key simply fails to
resolve, like a wrong one, rather than resolving and being caught
after the fact. New GET /api/users/{id}/api-keys (requireSelfOrAdmin,
same as create/delete) lists id/name/created_at/last_used_at/expires_at,
never the raw key.
Scoped down from the original plan on request: no web UI change, since
there turned out to be no existing API-keys UI at all to extend --
building one from scratch would have been a real feature addition, not
a hardening tweak.
Mirrored the additive expires_at field in terdut-tui's APIKey struct
(separate commit, separate repo) per this workspace's version-coupling
rule; the TUI does not create or list expiring keys itself yet.
New tests (api_keys_test.go): default never-expires, expires_in_days
sets expires_at, out-of-range values rejected, an expired key fails
auth after a fresh one worked, the listing never includes the raw key.
Also added the new GET route to authz_scope_test.go's self-or-admin
table from the previous commit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
50 lines
1.9 KiB
Go
50 lines
1.9 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// NtfyTopic is where this user's push notifications go. Nil means they get
|
|
// none of their own; incidents assigned to them fall back to the configured
|
|
// fallback topic instead.
|
|
NtfyTopic *string `json:"ntfy_topic,omitempty"`
|
|
|
|
// DisabledAt is when the account was taken out of use, or nil. A disabled
|
|
// user cannot authenticate by either credential, and keeps their name on
|
|
// every acknowledgement and timeline entry they made.
|
|
DisabledAt *time.Time `json:"disabled_at,omitempty"`
|
|
|
|
// IsAdmin is the system administrator flag: managing users and API keys.
|
|
// Not omitempty — a client has to be able to tell "false" from "this server
|
|
// is too old to have the field", and the web UI decides what to show from
|
|
// it.
|
|
IsAdmin bool `json:"is_admin"`
|
|
|
|
// AdminSource is who granted the flag: "manual" or "oidc". An "oidc"
|
|
// administrator follows the identity provider's groups, so the UI shows it as
|
|
// managed there and the API refuses to revoke it by hand. Only set on the
|
|
// user endpoints that show it.
|
|
AdminSource string `json:"admin_source,omitempty"`
|
|
}
|
|
|
|
type APIKey struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
Name string `json:"name"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
|
|
// ExpiresAt is nil for a key that never expires, which is every key
|
|
// created before this field existed and still the default for a new one
|
|
// unless its creator asks otherwise (see handleCreateAPIKey's
|
|
// expires_in_days). AuthMiddleware stops accepting a key once this
|
|
// passes; nothing deletes the row for it.
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
|
|
Key string `json:"key,omitempty"` // populated only on creation, never stored
|
|
}
|