feat: add initial admin user creation
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
744090aa9a
commit
6ce05ea74d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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")
|
|
@ -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
12
run.go
|
@ -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{}
|
||||
|
|
Loading…
Reference in New Issue