Admin name at init, token rotation, read-only forms, token role presets

- stewardd init asks for the first administrator's name on a terminal.
- Tokens can be rotated (steward token rotate, POST /tokens/{id}/rotate,
  Rotate in the web UI), with a grace period for the old secret, as in
  Jarl. Schema migration 2 adds the token columns.
- Read-only users see the settings and a subnet's settings locked,
  instead of editable fields without a Save button.
- The New token dialog only offers roles within what the user may do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 13:42:37 +01:00
1 parent db3f245962
commit 8a69038ab5
23 files changed
+418 -28

No files matched your search

+13 -2
View File
@@ -150,13 +150,21 @@ func (s *Server) tokenPrincipal(ctx context.Context, tok, ip, agent string) (*au
if err != nil {
return nil, err
}
now := time.Now()
if !auth.CheckSecret(secret, t.SecretHash) {
return nil, errors.New("wrong token secret")
// After a rotation with a grace period the old secret works until
// the grace period ends.
if t.PrevSecretHash == "" || t.PrevExpiresAt == nil || !auth.CheckSecret(secret, t.PrevSecretHash) {
return nil, errors.New("wrong token secret")
}
if now.After(*t.PrevExpiresAt) {
return nil, errors.New("this is the token's old secret, from before it was rotated; its grace period has ended")
}
s.log.Warn("token used with its old secret; give this client the new one before the grace period ends", "token_id", t.ID, "token", t.Username+"/"+t.Name, "ip", ip, "valid_until", t.PrevExpiresAt.Format(time.RFC3339))
}
if t.RevokedAt != nil {
return nil, errors.New("the token was revoked")
}
now := time.Now()
if t.ExpiresAt != nil && now.After(*t.ExpiresAt) {
return nil, errors.New("the token has expired")
}
@@ -271,6 +279,9 @@ func (s *Server) whoamiOf(p *auth.Principal, se *model.Session) api.Whoami {
if se != nil {
out.CSRFToken = se.CSRFToken
}
if p.Token != nil {
out.TokenID = p.Token.ID
}
return out
}