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:
@@ -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})
|
||||
}
|
||||
Reference in New Issue
Block a user