wk2: account management tooling
This commit is contained in:
parent
37eeeb2467
commit
f864d9c84e
34 changed files with 379 additions and 350 deletions
3
src/account-manager/go.mod
Normal file
3
src/account-manager/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module jupiter-mail.org/account-manager
|
||||
|
||||
go 1.23
|
||||
136
src/account-manager/main.go
Normal file
136
src/account-manager/main.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"jupiter-mail.org/account-manager/model"
|
||||
)
|
||||
|
||||
//go:embed res/help.txt
|
||||
var helpText string
|
||||
|
||||
var USERDB_PATH = path.Clean("/etc/dovecot/userdb")
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, helpText)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "create":
|
||||
account, err := createAccount(os.Args[2], os.Args[3])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create account: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf("Account \"%s\" created successfully.", account.FullUsername())
|
||||
case "list":
|
||||
log.Fatalf("list: not implemented")
|
||||
os.Exit(1)
|
||||
case "update":
|
||||
log.Fatalf("update: not implemented")
|
||||
os.Exit(1)
|
||||
case "delete":
|
||||
log.Fatalf("delete: not implemented")
|
||||
os.Exit(1)
|
||||
default:
|
||||
log.Fatalf("Unrecognised action \"%s\"", os.Args[1])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func openUserDB() (*os.File, error) {
|
||||
err := os.MkdirAll(path.Dir(USERDB_PATH), 0755)
|
||||
if err != nil { return nil, err }
|
||||
|
||||
file, err := os.OpenFile(USERDB_PATH, os.O_CREATE | os.O_RDWR, 0640)
|
||||
if err != nil && errors.Is(err, os.ErrExist) { return nil, err }
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func getMailHome(user string, domain string) string {
|
||||
return fmt.Sprintf("/var/mail/%s/%s/home", domain, user)
|
||||
}
|
||||
|
||||
func checkAccountExists(username string) (bool, error) {
|
||||
file, err := openUserDB()
|
||||
if err != nil { return false, fmt.Errorf("open userdb: %v", err) }
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil { return false, fmt.Errorf("read userdb: %v", err) }
|
||||
file.Close()
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, username) { return true, nil }
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func createAccount(fullUsername string, password string) (*model.MailAccount, error) {
|
||||
exists, err := checkAccountExists(fullUsername)
|
||||
if err != nil { return nil, fmt.Errorf("failed to check if account exists: %v", err) }
|
||||
if exists { return nil, fmt.Errorf("account already exists: %s", fullUsername) }
|
||||
|
||||
usernameSplits := strings.Split(fullUsername, "@")
|
||||
if len(usernameSplits) != 2 {
|
||||
return nil, fmt.Errorf("invalid email format: %s", fullUsername)
|
||||
}
|
||||
username := usernameSplits[0]
|
||||
domain := usernameSplits[1]
|
||||
|
||||
passwordHash, err := hashPassword(username, password)
|
||||
if err != nil { return nil, fmt.Errorf("hash password: %v", err) }
|
||||
|
||||
account := model.NewMailAccount(username, domain, []byte(passwordHash))
|
||||
|
||||
uid := 5000; gid := 5000
|
||||
// <username>:<password>:5000:5000::/var/mail/<domain>/<user>/home::
|
||||
userDbLine := fmt.Sprintf(
|
||||
"%s:%s:%d:%d::%s::\n",
|
||||
account.FullUsername(),
|
||||
account.PasswordHash,
|
||||
uid, gid,
|
||||
getMailHome(account.Username, account.Domain),
|
||||
)
|
||||
|
||||
file, err := openUserDB()
|
||||
if err != nil { return nil, fmt.Errorf("open userdb: %v", err) }
|
||||
buf := make([]byte, 1024)
|
||||
for n, err := file.Read(buf); n > 0; {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read userdb: %v", err)
|
||||
}
|
||||
}
|
||||
_, err = file.Write([]byte(userDbLine))
|
||||
if err != nil { return nil, err }
|
||||
|
||||
err = file.Close()
|
||||
if err != nil { return nil, err }
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func hashPassword(username string, password string) (string, error) {
|
||||
// doveadm pw -s SHA512-CRYPT -u "$USERNAME" -p "$PASSWORD"
|
||||
hashCmd := exec.Command(
|
||||
"doveadm", "pw",
|
||||
"-s", "SHA512-CRYPT",
|
||||
"-u", username,
|
||||
"-p", password,
|
||||
)
|
||||
out, err := hashCmd.Output()
|
||||
if err != nil { return "", err }
|
||||
return string(out), nil
|
||||
}
|
||||
23
src/account-manager/model/account.go
Normal file
23
src/account-manager/model/account.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
MailAccount struct {
|
||||
Username string `json:"username"`
|
||||
Domain string `json:"domain"`
|
||||
PasswordHash []byte `json:"password"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewMailAccount(username string, domain string, passwordHash []byte) MailAccount {
|
||||
return MailAccount{
|
||||
Username: username,
|
||||
Domain: domain,
|
||||
PasswordHash: passwordHash,
|
||||
}
|
||||
}
|
||||
|
||||
func (account *MailAccount) FullUsername() string {
|
||||
return fmt.Sprintf("%s@%s", account.Username, account.Domain)
|
||||
}
|
||||
7
src/account-manager/res/help.txt
Normal file
7
src/account-manager/res/help.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
usage: accounts <action> [...opts]
|
||||
|
||||
actions:
|
||||
create <username> <password>
|
||||
list
|
||||
update <username> <password>
|
||||
delete <username>
|
||||
Loading…
Add table
Add a link
Reference in a new issue