d7d9d45b45
Add a new flag to disable users. This can be useful to temporarily deactivate an account without erasing data. The user goroutine is kept alive for simplicity's sake. Most of the infrastructure assumes that each user always has a running goroutine. A disabled user's goroutine is responsible for sending back an error to downstream connections, and listening for potential events to re-enable the account.
151 lines
3.1 KiB
Go
151 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
"git.sr.ht/~emersion/soju/config"
|
|
"git.sr.ht/~emersion/soju/database"
|
|
)
|
|
|
|
const usage = `usage: sojuctl [-config path] <action> [options...]
|
|
|
|
create-user <username> [-admin] Create a new user
|
|
change-password <username> Change password for a user
|
|
help Show this help message
|
|
`
|
|
|
|
func init() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(flag.CommandLine.Output(), usage)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
var configPath string
|
|
flag.StringVar(&configPath, "config", "", "path to configuration file")
|
|
flag.Parse()
|
|
|
|
var cfg *config.Server
|
|
if configPath != "" {
|
|
var err error
|
|
cfg, err = config.Load(configPath)
|
|
if err != nil {
|
|
log.Fatalf("failed to load config file: %v", err)
|
|
}
|
|
} else {
|
|
cfg = config.Defaults()
|
|
}
|
|
|
|
db, err := database.Open(cfg.DB.Driver, cfg.DB.Source)
|
|
if err != nil {
|
|
log.Fatalf("failed to open database: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch cmd := flag.Arg(0); cmd {
|
|
case "create-user":
|
|
username := flag.Arg(1)
|
|
if username == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
fs := flag.NewFlagSet("", flag.ExitOnError)
|
|
admin := fs.Bool("admin", false, "make the new user admin")
|
|
fs.Parse(flag.Args()[2:])
|
|
|
|
password, err := readPassword()
|
|
if err != nil {
|
|
log.Fatalf("failed to read password: %v", err)
|
|
}
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
|
|
if err != nil {
|
|
log.Fatalf("failed to hash password: %v", err)
|
|
}
|
|
|
|
user := database.User{
|
|
Username: username,
|
|
Password: string(hashed),
|
|
Admin: *admin,
|
|
Enabled: true,
|
|
}
|
|
if err := db.StoreUser(ctx, &user); err != nil {
|
|
log.Fatalf("failed to create user: %v", err)
|
|
}
|
|
case "change-password":
|
|
username := flag.Arg(1)
|
|
if username == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
user, err := db.GetUser(ctx, username)
|
|
if err != nil {
|
|
log.Fatalf("failed to get user: %v", err)
|
|
}
|
|
|
|
password, err := readPassword()
|
|
if err != nil {
|
|
log.Fatalf("failed to read password: %v", err)
|
|
}
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
|
|
if err != nil {
|
|
log.Fatalf("failed to hash password: %v", err)
|
|
}
|
|
|
|
user.Password = string(hashed)
|
|
if err := db.StoreUser(ctx, user); err != nil {
|
|
log.Fatalf("failed to update password: %v", err)
|
|
}
|
|
default:
|
|
flag.Usage()
|
|
if cmd != "help" {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readPassword() ([]byte, error) {
|
|
var password []byte
|
|
var err error
|
|
fd := int(os.Stdin.Fd())
|
|
|
|
if terminal.IsTerminal(fd) {
|
|
fmt.Printf("Password: ")
|
|
password, err = terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Printf("\n")
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Warning: Reading password from stdin.\n")
|
|
// TODO: the buffering messes up repeated calls to readPassword
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
if !scanner.Scan() {
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
password = scanner.Bytes()
|
|
|
|
if len(password) == 0 {
|
|
return nil, fmt.Errorf("zero length password")
|
|
}
|
|
}
|
|
|
|
return password, nil
|
|
}
|