Multiple users, all admins

The single admin account becomes a list of users; config.json moves to
version 2 and the old admin is migrated on first start. Every user is an
admin. Sessions are tied to a user and their password, so deleting a user
or resetting a password signs them out at once. API tokens belong to the
user who made them and go away with that user.

Admins add users with a temporary password and choose whether it must be
changed at first sign-in; until then the API refuses everything but the
password change. Settings gets My account and Users cards, and the token
table shows each token's owner. 'GHOSTWIRE passwd [username]' resets any
user's password. A failed update now also restores config.json, since the
new version may have upgraded it.
This commit is contained in:
Daniel Redetzke
2026-10-04 15:51:14 +03:00
parent f703618b9d
commit 4ecfddf06a
11 changed files with 825 additions and 109 deletions
+27 -9
View File
@@ -53,7 +53,7 @@ func main() {
flag.Usage = usage
configPath := flag.String("config", defaultConfigPath(), "path to config.json; logs and stats are kept next to it")
passwd := flag.Bool("passwd", false, "set the admin password and exit")
passwd := flag.Bool("passwd", false, "set a user's password and exit (username as argument; default: the first user)")
down := flag.Bool("down", false, "remove the WireGuard interface and firewall rules and exit")
check := flag.Bool("check", false, "check that config.json is valid for this version and exit")
showVersion := flag.Bool("version", false, "print the version and exit")
@@ -66,7 +66,7 @@ func main() {
var err error
switch {
case *passwd:
if err = setPassword(*configPath); err == nil {
if err = setPassword(*configPath, flag.Arg(0)); err == nil {
fmt.Fprintf(os.Stderr, "If the service is running: systemctl reload %s\n", serviceName)
}
case *down:
@@ -106,12 +106,25 @@ func readSecret(prompt string) (string, error) {
return strings.TrimRight(line, "\r\n"), err
}
func setPassword(path string) error {
// setPassword sets a user's password from the terminal; it is the way back
// in for someone locked out. An empty username means the first user.
func setPassword(path, username string) error {
store, err := openStore(path)
if err != nil {
return err
}
pw, err := readSecret("New admin password: ")
cfg := store.Get()
u := &cfg.Users[0]
if username != "" {
if u = cfg.userByName(username); u == nil {
names := make([]string, len(cfg.Users))
for i, x := range cfg.Users {
names[i] = x.Username
}
return fmt.Errorf("no user %q; users: %s", username, strings.Join(names, ", "))
}
}
pw, err := readSecret(fmt.Sprintf("New password for %q: ", u.Username))
if err != nil {
return err
}
@@ -131,10 +144,15 @@ func setPassword(path string) error {
if err != nil {
return err
}
if err := store.Update(func(c *Config) error { c.Admin.PasswordHash = hash; return nil }); err != nil {
id := u.ID
if err := store.Update(func(c *Config) error {
_, u := c.userByID(id)
u.PasswordHash, u.MustChangePassword = hash, false
return nil
}); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Password for %q saved.\n", store.Get().Admin.Username)
fmt.Fprintf(os.Stderr, "Password for %q saved.\n", u.Username)
return nil
}
@@ -167,9 +185,9 @@ func run(configPath string) error {
}
defer logw.Close()
slog.Info("starting", "version", version, "config", configPath)
if cfg.Admin.PasswordHash == "" {
slog.Warn("no admin password set; run: sudo " + installBin + " passwd")
fmt.Fprintf(os.Stderr, "No admin password set. Run: sudo %s passwd\n", installBin)
if !cfg.passwordSet() {
slog.Warn("no password set; run: sudo " + installBin + " passwd")
fmt.Fprintf(os.Stderr, "No password set. Run: sudo %s passwd\n", installBin)
}
kernel, err := newKernel()