feat: add initial admin user creation
All checks were successful
continuous-integration/drone/push Build is passing

have the app create the initial admin user:
* if the db has not yet been set up
* if there are not users
* if the config value for Init.CreateAdmin is True
* if the admin password is not empty

default username, email values can be seen in modules/user/const.go
This commit is contained in:
leo 2023-05-21 18:50:41 +02:00
parent 744090aa9a
commit 6ce05ea74d
Signed by: wanderer
SSH Key Fingerprint: SHA256:Dp8+iwKHSlrMEHzE3bJnPng70I7LEsa3IJXRH/U+idQ
7 changed files with 180 additions and 26 deletions

View File

@ -23,6 +23,8 @@ type Settings struct {
httpRateLimit int
isLive bool
isDevel bool
initCreateAdmin bool
initAdminPassword string
loggerJSON bool
sessionCookieName string
sessionCookieAuthSecret string
@ -47,6 +49,7 @@ var cleantgt = []string{
"PCMT_DBTYPE",
"PCMT_SESSION_AUTH_SECRET",
"PCMT_SESSION_ENCR_SECRET",
"PCMT_INIT_ADMIN_PASSWORD",
}
// New returns a new instance of the settings struct.
@ -87,9 +90,6 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
s.SessionEncrIsHex = true
}
s.SetHTTPDomain(conf.HTTP.Domain)
s.SetHTTPSecure(conf.HTTP.Secure)
if conf.HTTP.Gzip > 0 {
s.SetHTTPGzipEnabled(true)
s.SetHTTPGzipLevel(conf.HTTP.Gzip)
@ -100,6 +100,14 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
s.SetHTTPRateLimit(conf.HTTP.RateLimit)
}
if conf.Init.CreateAdmin {
s.SetInitCreateAdmin(true)
s.SetInitAdminPassword(conf.Init.AdminPassword)
}
s.SetHTTPDomain(conf.HTTP.Domain)
s.SetHTTPSecure(conf.HTTP.Secure)
log.Debug("checking flag overrides")
overrideMsg := "overriding '%s' based on a flag: %+v"
@ -158,6 +166,16 @@ func (s *Settings) IsDevel() bool {
return s.isDevel
}
// InitCreateAdmin returns the value of initCreateAdmin of the receiver.
func (s *Settings) InitCreateAdmin() bool {
return s.initCreateAdmin
}
// InitAdminPassword returns the value of initAdminPassword of the receiver.
func (s *Settings) InitAdminPassword() string {
return s.initAdminPassword
}
// LoggerIsJSON returns whether the logger should use the JSON handler.
func (s *Settings) LoggerIsJSON() bool {
return s.loggerJSON
@ -263,6 +281,16 @@ func (s *Settings) SetIsDevel(devel bool) {
s.isDevel = devel
}
// SetInitCreateAdmin sets the value of initCreateAdmin of the receiver.
func (s *Settings) SetInitCreateAdmin(create bool) {
s.initCreateAdmin = create
}
// SetInitAdminPassword sets the value of initAdminPassword of the receiver.
func (s *Settings) SetInitAdminPassword(password string) {
s.initAdminPassword = password
}
// SetLoggerIsJSON sets the setting value of loggerIsJSON.
func (s *Settings) SetLoggerIsJSON(isJSON bool) {
s.loggerJSON = isJSON

View File

@ -92,6 +92,7 @@ var (
errSessionAuthSecretWrongSize = fmt.Errorf("session cookie authentication secret should be *exactly* 64 bytes long, both raw and hex-encoded strings are accepted; make sure to generate the key with sufficient entropy e.g. using openssl")
errSessionEncrSecretWrongSize = fmt.Errorf("session cookie encryption secret should be *exactly* 32 bytes (for 256 bit AES), both raw and hex-encoded strings are accepted; make sure to generate the key with sufficient entropy e.g. using openssl")
errSessionSecretZeros = fmt.Errorf("session cookie secrets cannot be all zeros")
errInitAdminPasswdEmpty = fmt.Errorf("requested initial admin creation and the initial admin password is empty")
// authSecretIsHex is used to recall whether the authentication secret was
// determined to be pure hex.
@ -177,6 +178,10 @@ func Load(conf string, isPath bool) (*Config, error) {
return nil, err
}
if config.Init.CreateAdmin && config.Init.AdminPassword == "" {
return nil, errInitAdminPasswdEmpty
}
return &config, nil
}

View File

@ -5,9 +5,11 @@ package db
import (
"context"
"fmt"
"git.dotya.ml/mirre-mt/pcmt/ent"
"git.dotya.ml/mirre-mt/pcmt/ent/migrate"
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
"git.dotya.ml/mirre-mt/pcmt/slogging"
"golang.org/x/exp/slog"
)
@ -15,8 +17,8 @@ import (
// CtxKey serves as a key to context values for this package.
type CtxKey struct{}
// DropAll deletes an re-creates the whole db.
func DropAll(ctx context.Context, client *ent.Client) error {
// migrateRecreateColIdx deletes and re-creates db schema.
func migrateRecreateColIdx(ctx context.Context, client *ent.Client) error { //nolint: unused
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
@ -24,6 +26,20 @@ func DropAll(ctx context.Context, client *ent.Client) error {
slog.Group("pcmt extra", slog.String("module", "modules/db")),
)
// deleting data is very drastic and should be done by the user.
// if setup is already found, simply bail.
// _, err := client.Setup.Delete().Exec(ctx)
// if err != nil {
// log.Errorf("failed to delete setup table: %v", err)
// return err
// }
//
// _, err = client.User.Delete().Exec(ctx)
// if err != nil {
// log.Errorf("failed to delete user table: %v", err)
// return err
// }
err := client.Schema.
Create(
ctx,
@ -38,7 +54,7 @@ func DropAll(ctx context.Context, client *ent.Client) error {
return err
}
// IsSetUp deletes the whole db.
// IsSetUp checks if the database has previously been set up.
func IsSetUp(ctx context.Context, client *ent.Client) (bool, error) {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
@ -52,7 +68,7 @@ func IsSetUp(ctx context.Context, client *ent.Client) (bool, error) {
Only(ctx)
if is != nil {
log.Debug("apparently the db is already set up")
log.Debug("apparently the db has already been set up")
} else {
log.Debug("apparently the db was not yet set up")
}
@ -74,8 +90,15 @@ func IsSetUp(ctx context.Context, client *ent.Client) (bool, error) {
return false, nil
}
// SetUp creates the set-up record indicating that the DB has been set up.
func SetUp(ctx context.Context, client *ent.Client) error {
// SetUp attempts to automatically migrate DB schema and creates the set-up
// record indicating that the DB has been set up. Optionally and only if the DB
// has not been set up prior, it creates and admin user with the initPasswd and
// the default username and email (see ../user/const.go).
func SetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
if err := client.Schema.Create(ctx); err != nil {
return fmt.Errorf("failed to create schema resources: %v", err)
}
isSetup, err := IsSetUp(ctx, client)
switch {
@ -85,20 +108,55 @@ func SetUp(ctx context.Context, client *ent.Client) error {
case err != nil && isSetup:
return err
case err != nil && !isSetup:
err = DropAll(ctx, client)
case err == nil && !isSetup:
// run the setup in a transaction.
tx, err := client.Tx(ctx)
if err != nil {
return nil
return err
}
// create a set-up record.
_, err = client.Setup.
Create().
Save(ctx)
if err != nil {
txClient := tx.Client()
usrCtx := context.WithValue(context.Background(),
moduser.CtxKey{}, ctx.Value(CtxKey{}).(*slogging.Slogger),
)
// use the "doSetUp" below, but give it the transactional client; no
// code changes to "doSetUp".
if err := doSetUp(usrCtx, txClient, createAdmin, initPasswd); err != nil {
return rollback(tx, err)
}
return tx.Commit()
}
// the remaining scenario (not set up and err) simply returns the err.
return err
}
func doSetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
if _, err := client.Setup.
Create().
Save(ctx); err != nil {
return err
}
if createAdmin {
if err := moduser.CreateFirst(ctx, client, moduser.AdminUname, moduser.AdminEmail, initPasswd); err != nil {
return err
}
}
return nil
}
// rollback calls to tx.Rollback and wraps the given error with the rollback
// error if occurred.
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: %v", err, rerr)
}
return err
}

11
modules/user/const.go Normal file
View File

@ -0,0 +1,11 @@
// Copyright 2023 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: AGPL-3.0-only
package user
const (
// AdminUname is the username of the initial administrator user.
AdminUname = "admin"
// AdminEmail is the email of the initial administrator user.
AdminEmail = "admin@adminmail.admindomain"
)

8
modules/user/error.go Normal file
View File

@ -0,0 +1,8 @@
// Copyright 2023 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: AGPL-3.0-only
package user
import "errors"
var ErrUsersAlreadyPresent = errors.New("don't call CreateFirst when there already are another users")

View File

@ -28,7 +28,7 @@ type User struct {
}
// CreateUser adds a user entry to the database.
func CreateUser(ctx context.Context, client *ent.Client, email, username, password string) (*ent.User, error) {
func CreateUser(ctx context.Context, client *ent.Client, email, username, password string, isAdmin ...bool) (*ent.User, error) {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
@ -42,11 +42,19 @@ func CreateUser(ctx context.Context, client *ent.Client, email, username, passwo
return nil, errors.New("could not hash password")
}
var admin bool
// if set, the first of the array is the arg.
if len(isAdmin) != 0 {
admin = isAdmin[0]
}
u, err := client.User.
Create().
SetEmail(email).
SetUsername(username).
SetPassword(digest).
SetIsAdmin(admin).
Save(ctx)
switch {
@ -215,3 +223,43 @@ func EmailExists(ctx context.Context, client *ent.Client, email string) (bool, e
return false, nil
}
// NoUsers checks whether there are any users at all in the db.
func NoUsers(ctx context.Context, client *ent.Client) (bool, error) {
count, err := client.User.
Query().
Count(ctx)
if err != nil {
return false, nil
}
if count > 0 {
return false, nil
}
return true, nil
}
// CreateFirst creates the first user and makes them an administrator.
// To be used during app setup.
func CreateFirst(ctx context.Context, client *ent.Client, username, email, password string) error {
noUsers, err := NoUsers(ctx, client)
switch {
case err != nil:
return err
case noUsers:
_, err := CreateUser(ctx, client, email, username, password, true)
if err != nil {
return err
}
return nil
case !noUsers:
return ErrUsersAlreadyPresent
}
return err
}

12
run.go
View File

@ -143,18 +143,14 @@ func run() error {
ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger)
log.Info("making sure that the db is set up")
log.Info("ensuring the db is set up and attempting to automatically migrate db schema")
if err = moddb.SetUp(ctx, db); err != nil {
// make sure the database is set up and optionally creates an administrator
// user (only when setting up the db).
if err = moddb.SetUp(ctx, db, setting.InitCreateAdmin(), setting.InitAdminPassword()); err != nil {
return err
}
log.Info("attempting to automatically migrate db schema")
// Run the auto migration tool.
if err = db.Schema.Create(context.Background()); err != nil {
return fmt.Errorf("failed creating schema resources: %v", err)
}
setting.SetDbIsSetUp(true)
a := &app.App{}