A guest login: temporary, look-only, revocable
Users -> Guest logins makes a throwaway user name and password, good for half an hour by default, to hand to somebody who wants to see the console. It is a normal account with `guest: true` and an expiry, so login, sessions and the saved look need no special case, and it is left out of the account list, out of "the last account can't be removed", and out of the one account an empty user name means. What a guest may do is one allow-list, checked in the auth middleware, rather than a flag ~190 handlers have to remember. The list is matched by asking the router which handler a request would reach, so it cannot drift from the routes it names, and it is checked against the scoped path — /api/nodes/grid/procs is /api/procs on grid — because the hub proxies to a node with a token that is full control of that node. The node cannot tell a guest from its owner, so the only place it can be stopped is here, before the request leaves. A guest gets the dashboard, containers, stacks, disks, hardware, ZFS and usage, on this machine and on any managed one, and nothing else: no terminal (attaching is a GET, and it is a root shell), no files, no container logs or environments, no stack files, no alert channels, no cron, no tokens, no settings, no power. Expiry is enforced on every request and swept once a minute, so a console left open on a visitor's phone stops within the minute rather than at their next click; revoking signs them out at once. The console hides what a guest cannot use and says how long is left where the power button would be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4450f1285d
commit
8a7167bf03
@@ -46,6 +46,22 @@ type User struct {
|
||||
Hash string `json:"hash"`
|
||||
Created time.Time `json:"created"`
|
||||
LastSeen time.Time `json:"last_seen,omitempty"`
|
||||
// Guest marks a temporary login made to show somebody the console: it may only read,
|
||||
// it expires by itself at Expires, and it is not one of the machine's accounts — it is
|
||||
// left out of the account list, does not count towards "the last account", and cannot
|
||||
// be the one an empty user name means. See internal/api/guests.go.
|
||||
Guest bool `json:"guest,omitempty"`
|
||||
// Expires is when a guest login stops working. A guest without one has already
|
||||
// expired, which is the safe way to read a half-written file.
|
||||
Expires time.Time `json:"expires,omitempty"`
|
||||
// IssuedBy is the account that made this guest login.
|
||||
IssuedBy string `json:"issued_by,omitempty"`
|
||||
}
|
||||
|
||||
// Expired reports whether a guest login's time is up. A real account never expires; a
|
||||
// guest with no expiry at all counts as expired rather than as forever.
|
||||
func (u User) Expired(now time.Time) bool {
|
||||
return u.Guest && (u.Expires.IsZero() || now.After(u.Expires))
|
||||
}
|
||||
|
||||
// UpdateSource is where this machine looks for a newer Ham Lab, and how often. The field
|
||||
@@ -82,8 +98,8 @@ type Config struct {
|
||||
Listen string `json:"listen"`
|
||||
// PasswordHash is the single login of installs before users existed. On load it becomes
|
||||
// the first user (see MigrateUsers) and is then dropped from the file.
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
Users []User `json:"users,omitempty"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
Users []User `json:"users,omitempty"`
|
||||
TLS string `json:"tls,omitempty"` // "auto" → self-signed cert in the data dir; "" → plain HTTP
|
||||
Tokens []Token `json:"tokens,omitempty"`
|
||||
Nodes []Node `json:"nodes,omitempty"`
|
||||
@@ -145,6 +161,18 @@ func (c *Config) MigrateUsers(name string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Accounts is every real account: the machine's users, without the guest logins.
|
||||
// Call under View/Update or on a config nobody else holds.
|
||||
func (c *Config) Accounts() []User {
|
||||
out := make([]User, 0, len(c.Users))
|
||||
for _, u := range c.Users {
|
||||
if !u.Guest {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// User finds an account by name. Call under View/Update or on a config nobody else holds.
|
||||
func (c *Config) User(name string) (User, bool) {
|
||||
for _, u := range c.Users {
|
||||
|
||||
Reference in New Issue
Block a user