From f5cd385530fec1984159d79d3383b554ae81791f Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Mon, 11 Aug 2025 17:58:09 +0200 Subject: [PATCH] Implement galenectl initial-setup. --- galenectl/galenectl.go | 118 +++++++++++++++++++++++++++++++++++++++-- group/group.go | 14 ++--- 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/galenectl/galenectl.go b/galenectl/galenectl.go index cc4cc26..9a6fa79 100644 --- a/galenectl/galenectl.go +++ b/galenectl/galenectl.go @@ -31,13 +31,14 @@ import ( type configuration struct { Server string `json:"server"` - AdminUsername string `json:"admin-username"` - AdminPassword string `json:"admin-password"` - AdminToken string `json:"admin-token"` + AdminUsername string `json:"admin-username,omitempty"` + AdminPassword string `json:"admin-password,omitempty"` + AdminToken string `json:"admin-token,omitempty"` } var insecure bool var serverURL, adminUsername, adminPassword, adminToken string +var configFile string var client http.Client @@ -51,6 +52,10 @@ var commands = map[string]command{ command: hashPasswordCmd, description: "generate a hashed password", }, + "initial-setup": { + command: initialSetupCmd, + description: "initial setup of Galene and galenectl", + }, "set-password": { command: setPasswordCmd, description: "set a user's password", @@ -114,7 +119,7 @@ func main() { if err != nil { log.Fatalf("UserConfigDir: %v", err) } - configFile := filepath.Join( + configFile = filepath.Join( filepath.Join(configdir, "galene"), "galenectl.json", ) @@ -321,6 +326,111 @@ func hashPasswordCmd(cmdname string, args []string) { } } +func initialSetupCmd(cmdname string, args []string) { + var galeneConfigFn string + + cmd := flag.NewFlagSet(cmdname, flag.ExitOnError) + setUsage(cmd, cmdname, + "%v [option...] %v [option...]\n", + os.Args[0], cmdname, + ) + cmd.StringVar(&galeneConfigFn, "config", "config.json", + "Galene configuration `file`") + cmd.Parse(args) + + if cmd.NArg() != 0 { + cmd.Usage() + os.Exit(1) + } + + if adminUsername == "" { + fmt.Fprintf(cmd.Output(), + "Option \"-admin-username\" is required.\n", + ) + os.Exit(1) + } + + if adminPassword == "" && adminToken == "" { + fmt.Fprint(os.Stdin, "Administrator password: ") + pw, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + log.Fatalf("ReadPassword: %v", err) + } + adminPassword = string(pw) + fmt.Fprint(os.Stdin, "\n") + } + + galeneConfig, err := os.OpenFile(galeneConfigFn, + os.O_WRONLY|os.O_CREATE|os.O_CREATE|os.O_EXCL, + 0600) + if err != nil { + log.Fatalf("Create %v: %v", galeneConfigFn, err) + } + + galenectlConfig, err := os.OpenFile(configFile, + os.O_WRONLY|os.O_CREATE|os.O_CREATE|os.O_EXCL, + 0600) + if err != nil { + galeneConfig.Close() + os.Remove(galeneConfigFn) + log.Fatalf("Create %v: %v", galeneConfigFn, err) + } + + defer galeneConfig.Close() + defer galenectlConfig.Close() + + var users map[string]group.UserDescription + if adminPassword != "" { + pw, err := makePassword(adminPassword, "bcrypt", 0, 0, 0, 12) + if err != nil { + log.Fatalf("makePassword: %v", err) + } + + perms, err := group.NewPermissions("admin") + if err != nil { + log.Fatalf("NewPermissions: %v", err) + } + users = map[string]group.UserDescription{ + adminUsername: { + Password: pw, + Permissions: perms, + }, + } + } + + config := group.Configuration{ + WritableGroups: true, + Users: users, + } + + encoder := json.NewEncoder(galeneConfig) + encoder.SetIndent("", " ") + err = encoder.Encode(&config) + if err != nil { + log.Fatalf("Encode %v: %v", galeneConfigFn, err) + } + + ctlConfig := configuration{ + Server: serverURL, + AdminUsername: adminUsername, + AdminPassword: adminPassword, + AdminToken: adminToken, + } + + ctlEncoder := json.NewEncoder(galenectlConfig) + ctlEncoder.SetIndent("", " ") + err = ctlEncoder.Encode(&ctlConfig) + if err != nil { + log.Fatalf("Encode %v: %v", configFile, err) + } + + fmt.Printf("The file %v has been created. ", galeneConfigFn) + fmt.Printf("Please copy it into your server's\n\"data/\" directory.\n") + fmt.Printf("The file %v has been created. ", configFile) + fmt.Printf("It contains the administrator's\npassword in cleartext, ") + fmt.Printf("please keep it secure.\n") +} + func setAuthorization(req *http.Request) { if adminToken != "" { req.Header.Set("Authorization", "Bearer "+adminToken) diff --git a/group/group.go b/group/group.go index 7490ae5..9189d3b 100644 --- a/group/group.go +++ b/group/group.go @@ -917,15 +917,15 @@ type Configuration struct { modTime time.Time `json:"-"` fileSize int64 `json:"-"` - CanonicalHost string `json:"canonicalHost"` - AllowOrigin []string `json:"allowOrigin"` - AllowAdminOrigin []string `json:"allowAdminOrigin"` - ProxyURL string `json:"proxyURL"` - WritableGroups bool `json:"writableGroups"` - Users map[string]UserDescription + CanonicalHost string `json:"canonicalHost,omitempty"` + AllowOrigin []string `json:"allowOrigin,omitempty"` + AllowAdminOrigin []string `json:"allowAdminOrigin,omitempty"` + ProxyURL string `json:"proxyURL,omitempty"` + WritableGroups bool `json:"writableGroups,omitempty"` + Users map[string]UserDescription `json:"users,omitempty"` // obsolete fields - Admin []ClientPattern `json:"admin"` + Admin []ClientPattern `json:"admin,omitempty"` } func (conf Configuration) Zero() bool {