Add users and network settings tabs with admin protections.

List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
This commit is contained in:
2026-07-18 15:18:01 +02:00
parent f049f766d9
commit 2605c3b346
49 changed files with 5440 additions and 209 deletions
+68 -64
View File
@@ -1,18 +1,11 @@
package settings
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)
type encryptedBlob struct {
Nonce string `json:"nonce"`
Ciphertext string `json:"ciphertext"`
}
// KeyFromEnv reads and decodes CLUSTERCANVAS_CONFIG_KEY (base64, 16/24/32 bytes).
func KeyFromEnv() ([]byte, error) {
rawKey := os.Getenv(ConfigKeyEnvVar)
@@ -24,67 +17,78 @@ func KeyFromEnv() ([]byte, error) {
// LoadSecrets decrypts secrets.enc from dir using key.
func LoadSecrets(dir string, key []byte) (Secrets, error) {
path := filepath.Join(dir, SecretsFileName)
payload, err := os.ReadFile(path)
if err != nil {
return Secrets{}, fmt.Errorf("read secrets: %w", err)
}
var blob encryptedBlob
if err := json.Unmarshal(payload, &blob); err != nil {
return Secrets{}, fmt.Errorf("parse secrets envelope: %w", err)
}
nonce, err := base64.StdEncoding.DecodeString(blob.Nonce)
if err != nil {
return Secrets{}, fmt.Errorf("decode nonce: %w", err)
}
ciphertext, err := base64.StdEncoding.DecodeString(blob.Ciphertext)
if err != nil {
return Secrets{}, fmt.Errorf("decode ciphertext: %w", err)
}
plaintext, err := open(nonce, ciphertext, key)
if err != nil {
return Secrets{}, err
}
var secrets Secrets
if err := json.Unmarshal(plaintext, &secrets); err != nil {
return Secrets{}, fmt.Errorf("parse secrets: %w", err)
if err := loadEncryptedJSON(dir, SecretsFileName, key, &secrets); err != nil {
return Secrets{}, err
}
return secrets, nil
}
// SaveSecrets encrypts secrets and writes secrets.enc to dir with mode 0600.
func SaveSecrets(dir string, secrets Secrets, key []byte) error {
if err := ensureConfigDir(dir); err != nil {
return err
}
plaintext, err := json.Marshal(secrets)
if err != nil {
return fmt.Errorf("encode secrets: %w", err)
}
nonce, ciphertext, err := seal(plaintext, key)
if err != nil {
return err
}
blob := encryptedBlob{
Nonce: base64.StdEncoding.EncodeToString(nonce),
Ciphertext: base64.StdEncoding.EncodeToString(ciphertext),
}
payload, err := json.MarshalIndent(blob, "", " ")
if err != nil {
return fmt.Errorf("encode secrets envelope: %w", err)
}
payload = append(payload, '\n')
path := filepath.Join(dir, SecretsFileName)
if err := os.WriteFile(path, payload, 0o600); err != nil {
return fmt.Errorf("write secrets: %w", err)
}
return nil
return saveEncryptedJSON(dir, SecretsFileName, key, secrets)
}
// LoadPasswords decrypts passwords.enc from dir using key.
func LoadPasswords(dir string, key []byte) (PasswordStore, error) {
var store PasswordStore
if err := loadEncryptedJSON(dir, PasswordsFileName, key, &store); err != nil {
return PasswordStore{}, err
}
if store.Users == nil {
store.Users = []UserCredential{}
}
return store, nil
}
// LoadPasswordsOrEmpty returns an empty store when passwords.enc is missing.
func LoadPasswordsOrEmpty(dir string, key []byte) (PasswordStore, error) {
store, err := LoadPasswords(dir, key)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return PasswordStore{Users: []UserCredential{}}, nil
}
return PasswordStore{}, err
}
return store, nil
}
// SavePasswords encrypts the password store and writes passwords.enc.
func SavePasswords(dir string, store PasswordStore, key []byte) error {
if store.Users == nil {
store.Users = []UserCredential{}
}
return saveEncryptedJSON(dir, PasswordsFileName, key, store)
}
// LoadSessions decrypts sessions.enc from dir using key.
func LoadSessions(dir string, key []byte) (SessionStore, error) {
var store SessionStore
if err := loadEncryptedJSON(dir, SessionsFileName, key, &store); err != nil {
return SessionStore{}, err
}
if store.Sessions == nil {
store.Sessions = []SessionRecord{}
}
return store, nil
}
// LoadSessionsOrEmpty returns an empty store when sessions.enc is missing.
func LoadSessionsOrEmpty(dir string, key []byte) (SessionStore, error) {
store, err := LoadSessions(dir, key)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return SessionStore{Sessions: []SessionRecord{}}, nil
}
return SessionStore{}, err
}
return store, nil
}
// SaveSessions encrypts the session store and writes sessions.enc.
func SaveSessions(dir string, store SessionStore, key []byte) error {
if store.Sessions == nil {
store.Sessions = []SessionRecord{}
}
return saveEncryptedJSON(dir, SessionsFileName, key, store)
}