Add SSH-managed node registry with connection testing and reauth.

Register hosts under Containers/VMs/Docker with encrypted key storage, and require re-authentication for sensitive account changes.
This commit is contained in:
2026-07-18 16:39:10 +02:00
parent b93b7519ec
commit f4dc8f63d7
31 changed files with 4801 additions and 223 deletions
+88
View File
@@ -0,0 +1,88 @@
package api
import (
"context"
"errors"
"net/http"
"time"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/auth"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
)
const reauthRequiredMessage = "reauth_required"
type reauthRequest struct {
Password string `json:"password"`
TOTPCode string `json:"totp_code"`
}
func SessionFromContext(ctx context.Context) (settings.SessionRecord, bool) {
session, ok := ctx.Value(contextSessionKey).(settings.SessionRecord)
return session, ok
}
func verifyActorCredentials(user settings.UserCredential, password string, totpCode string, security settings.SecuritySettings) error {
ok, err := auth.VerifyPassword(password, user.PasswordHash)
if err != nil || !ok {
return errors.New("invalid credentials")
}
requiresTOTP := security.TotpEnabled && user.TOTPConfirmed && user.TOTPSecret != ""
if requiresTOTP {
if !auth.VerifyTOTPCode(user.TOTPSecret, totpCode) {
return errors.New("invalid or missing TOTP code")
}
}
return nil
}
func (app *App) requireInlineReauth(
request *http.Request,
password string,
totpCode string,
) error {
user, ok := UserFromContext(request.Context())
if !ok {
return errors.New("authentication required")
}
settingsPayload, err := loadSettingsOrDefault(app.ConfigDir)
if err != nil {
return err
}
security := effectiveSecurity(settingsPayload.Security)
return verifyActorCredentials(user, password, totpCode, security)
}
// requireGraceReauth returns an error with message reauth_required when the
// session is outside the grace window. When reauth_sensitive_actions is off, it
// is a no-op.
func (app *App) requireGraceReauth(request *http.Request) error {
settingsPayload, err := loadSettingsOrDefault(app.ConfigDir)
if err != nil {
return err
}
security := effectiveSecurity(settingsPayload.Security)
if !security.ReauthSensitiveActions {
return nil
}
session, ok := SessionFromContext(request.Context())
if !ok {
return errors.New("authentication required")
}
grace := time.Duration(security.ReauthGraceMinutes) * time.Minute
elapsed := time.Since(session.LastReauthAt)
// Allow a short post-reauth window so grace=0 still supports reauth-then-retry.
const justReauthedWindow = 30 * time.Second
if elapsed <= grace || elapsed <= justReauthedWindow {
return nil
}
return errors.New(reauthRequiredMessage)
}
func writeReauthRequired(writer http.ResponseWriter) {
writeJSON(writer, http.StatusUnauthorized, apiErrorResponse{Error: reauthRequiredMessage})
}