[chore] Admin CLI + new account creation refactoring (#2008)
* set maxPasswordLength to 72 bytes, rename validate function * refactor NewSignup * refactor admin account CLI commands * refactor oidc create user * refactor processor create * tweak password change, check old != new password
This commit is contained in:
parent
f8f0312042
commit
5a29a031ad
|
@ -19,7 +19,6 @@ package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
@ -28,88 +27,101 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action"
|
"github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
|
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create creates a new account in the database using the provided flags.
|
func initState(ctx context.Context) (*state.State, error) {
|
||||||
var Create action.GTSAction = func(ctx context.Context) error {
|
|
||||||
var state state.State
|
var state state.State
|
||||||
state.Caches.Init()
|
state.Caches.Init()
|
||||||
|
state.Caches.Start()
|
||||||
state.Workers.Start()
|
state.Workers.Start()
|
||||||
|
|
||||||
|
// Set the state DB connection
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return nil, fmt.Errorf("error creating dbConn: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
state.DB = dbConn
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
return &state, nil
|
||||||
if username == "" {
|
}
|
||||||
return errors.New("no username set")
|
|
||||||
|
func stopState(ctx context.Context, state *state.State) error {
|
||||||
|
if err := state.DB.Stop(ctx); err != nil {
|
||||||
|
return fmt.Errorf("error stopping dbConn: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.Workers.Stop()
|
||||||
|
state.Caches.Stop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new account and user
|
||||||
|
// in the database using the provided flags.
|
||||||
|
var Create action.GTSAction = func(ctx context.Context) error {
|
||||||
|
state, err := initState(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username := config.GetAdminAccountUsername()
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameAvailable, err := dbConn.IsUsernameAvailable(ctx, username)
|
usernameAvailable, err := state.DB.IsUsernameAvailable(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !usernameAvailable {
|
if !usernameAvailable {
|
||||||
return fmt.Errorf("username %s is already in use", username)
|
return fmt.Errorf("username %s is already in use", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := config.GetAdminAccountEmail()
|
email := config.GetAdminAccountEmail()
|
||||||
if email == "" {
|
|
||||||
return errors.New("no email set")
|
|
||||||
}
|
|
||||||
if err := validate.Email(email); err != nil {
|
if err := validate.Email(email); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
emailAvailable, err := dbConn.IsEmailAvailable(ctx, email)
|
emailAvailable, err := state.DB.IsEmailAvailable(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !emailAvailable {
|
if !emailAvailable {
|
||||||
return fmt.Errorf("email address %s is already in use", email)
|
return fmt.Errorf("email address %s is already in use", email)
|
||||||
}
|
}
|
||||||
|
|
||||||
password := config.GetAdminAccountPassword()
|
password := config.GetAdminAccountPassword()
|
||||||
if password == "" {
|
if err := validate.Password(password); err != nil {
|
||||||
return errors.New("no password set")
|
|
||||||
}
|
|
||||||
if err := validate.NewPassword(password); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = dbConn.NewSignup(ctx, username, "", false, email, password, nil, "", "", true, "", false)
|
if _, err := state.DB.NewSignup(ctx, gtsmodel.NewSignup{
|
||||||
if err != nil {
|
Username: username,
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
EmailVerified: true, // Assume cli user wants email marked as verified already.
|
||||||
|
PreApproved: true, // Assume cli user wants account marked as approved already.
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConn.Stop(ctx)
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all existing local accounts.
|
// List returns all existing local accounts.
|
||||||
var List action.GTSAction = func(ctx context.Context) error {
|
var List action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
users, err := state.DB.GetAllUsers(ctx)
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
users, err := dbConn.GetAllUsers(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -140,218 +152,182 @@ var List action.GTSAction = func(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm sets a user to Approved, sets Email to the current UnconfirmedEmail value, and sets ConfirmedAt to now.
|
// Confirm sets a user to Approved, sets Email to the current
|
||||||
|
// UnconfirmedEmail value, and sets ConfirmedAt to now.
|
||||||
var Confirm action.GTSAction = func(ctx context.Context) error {
|
var Confirm action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
username := config.GetAdminAccountUsername()
|
||||||
if username == "" {
|
|
||||||
return errors.New("no username set")
|
|
||||||
}
|
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := dbConn.GetAccountByUsernameDomain(ctx, username, "")
|
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := dbConn.GetUserByAccountID(ctx, a.ID)
|
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
updatingColumns := []string{"approved", "email", "confirmed_at"}
|
user.Approved = func() *bool { a := true; return &a }()
|
||||||
approved := true
|
user.Email = user.UnconfirmedEmail
|
||||||
u.Approved = &approved
|
user.ConfirmedAt = time.Now()
|
||||||
u.Email = u.UnconfirmedEmail
|
if err := state.DB.UpdateUser(
|
||||||
u.ConfirmedAt = time.Now()
|
ctx, user,
|
||||||
if err := dbConn.UpdateUser(ctx, u, updatingColumns...); err != nil {
|
"approved", "email", "confirmed_at",
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConn.Stop(ctx)
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promote sets a user to admin.
|
// Promote sets admin + moderator flags on a user to true.
|
||||||
var Promote action.GTSAction = func(ctx context.Context) error {
|
var Promote action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
username := config.GetAdminAccountUsername()
|
||||||
if username == "" {
|
|
||||||
return errors.New("no username set")
|
|
||||||
}
|
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := dbConn.GetAccountByUsernameDomain(ctx, username, "")
|
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := dbConn.GetUserByAccountID(ctx, a.ID)
|
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
admin := true
|
user.Admin = func() *bool { a := true; return &a }()
|
||||||
u.Admin = &admin
|
user.Moderator = func() *bool { a := true; return &a }()
|
||||||
if err := dbConn.UpdateUser(ctx, u, "admin"); err != nil {
|
if err := state.DB.UpdateUser(
|
||||||
|
ctx, user,
|
||||||
|
"admin", "moderator",
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConn.Stop(ctx)
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Demote sets admin on a user to false.
|
// Demote sets admin + moderator flags on a user to false.
|
||||||
var Demote action.GTSAction = func(ctx context.Context) error {
|
var Demote action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
username := config.GetAdminAccountUsername()
|
||||||
if username == "" {
|
|
||||||
return errors.New("no username set")
|
|
||||||
}
|
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := dbConn.GetAccountByUsernameDomain(ctx, username, "")
|
a, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := dbConn.GetUserByAccountID(ctx, a.ID)
|
user, err := state.DB.GetUserByAccountID(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
admin := false
|
user.Admin = func() *bool { a := false; return &a }()
|
||||||
u.Admin = &admin
|
user.Moderator = func() *bool { a := false; return &a }()
|
||||||
if err := dbConn.UpdateUser(ctx, u, "admin"); err != nil {
|
if err := state.DB.UpdateUser(
|
||||||
|
ctx, user,
|
||||||
|
"admin", "moderator",
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConn.Stop(ctx)
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable sets Disabled to true on a user.
|
// Disable sets Disabled to true on a user.
|
||||||
var Disable action.GTSAction = func(ctx context.Context) error {
|
var Disable action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
username := config.GetAdminAccountUsername()
|
||||||
if username == "" {
|
|
||||||
return errors.New("no username set")
|
|
||||||
}
|
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := dbConn.GetAccountByUsernameDomain(ctx, username, "")
|
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := dbConn.GetUserByAccountID(ctx, a.ID)
|
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
disabled := true
|
user.Disabled = func() *bool { d := true; return &d }()
|
||||||
u.Disabled = &disabled
|
if err := state.DB.UpdateUser(
|
||||||
if err := dbConn.UpdateUser(ctx, u, "disabled"); err != nil {
|
ctx, user,
|
||||||
|
"disabled",
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConn.Stop(ctx)
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password sets the password of target account.
|
// Password sets the password of target account.
|
||||||
var Password action.GTSAction = func(ctx context.Context) error {
|
var Password action.GTSAction = func(ctx context.Context) error {
|
||||||
var state state.State
|
state, err := initState(ctx)
|
||||||
state.Caches.Init()
|
|
||||||
state.Workers.Start()
|
|
||||||
|
|
||||||
dbConn, err := bundb.NewBunDBService(ctx, &state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state DB connection
|
|
||||||
state.DB = dbConn
|
|
||||||
|
|
||||||
username := config.GetAdminAccountUsername()
|
username := config.GetAdminAccountUsername()
|
||||||
if username == "" {
|
|
||||||
return errors.New("no username set")
|
|
||||||
}
|
|
||||||
if err := validate.Username(username); err != nil {
|
if err := validate.Username(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
password := config.GetAdminAccountPassword()
|
password := config.GetAdminAccountPassword()
|
||||||
if password == "" {
|
if err := validate.Password(password); err != nil {
|
||||||
return errors.New("no password set")
|
|
||||||
}
|
|
||||||
if err := validate.NewPassword(password); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := dbConn.GetAccountByUsernameDomain(ctx, username, "")
|
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := dbConn.GetUserByAccountID(ctx, a.ID)
|
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error hashing password: %s", err)
|
return fmt.Errorf("error hashing password: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.EncryptedPassword = string(pw)
|
user.EncryptedPassword = string(encryptedPassword)
|
||||||
return dbConn.UpdateUser(ctx, u, "encrypted_password")
|
if err := state.DB.UpdateUser(
|
||||||
|
ctx, user,
|
||||||
|
"encrypted_password",
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stopState(ctx, state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,50 +272,89 @@ func (m *Module) fetchUserForClaims(ctx context.Context, claims *oidc.Claims, ip
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, extraInfo *extraInfo, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
|
func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, extraInfo *extraInfo, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
|
||||||
// check if the email address is available for use; if it's not there's nothing we can so
|
// Check if the claimed email address is available for use.
|
||||||
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
|
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
err := gtserror.Newf("db error checking email availability: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !emailAvailable {
|
if !emailAvailable {
|
||||||
help := "The email address given to us by your authentication provider already exists in our records and the server administrator has not enabled account migration"
|
const help = "The email address given to us by your authentication provider already exists in our records and the server administrator has not enabled account migration"
|
||||||
return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email), help)
|
err := gtserror.Newf("email address %s is not available", claims.Email)
|
||||||
|
return nil, gtserror.NewErrorConflict(err, help)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the user is in any recognised admin groups
|
// We still need to set something as a password, even
|
||||||
adminGroups := config.GetOIDCAdminGroups()
|
// if it's not a password the user will end up using.
|
||||||
var admin bool
|
|
||||||
LOOP:
|
|
||||||
for _, g := range claims.Groups {
|
|
||||||
for _, ag := range adminGroups {
|
|
||||||
if strings.EqualFold(g, ag) {
|
|
||||||
admin = true
|
|
||||||
break LOOP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We still need to set *a* password even if it's not a password the user will end up using, so set something random.
|
|
||||||
// We'll just set two uuids on top of each other, which should be long + random enough to baffle any attempts to crack.
|
|
||||||
//
|
//
|
||||||
// If the user ever wants to log in using gts password rather than oidc flow, they'll have to request a password reset, which is fine
|
// We'll just set two uuids on top of each other, which
|
||||||
|
// should be long + random enough to baffle any attempts
|
||||||
|
// to crack, and which is also, conveniently, 72 bytes,
|
||||||
|
// which is the maximum length that bcrypt can handle.
|
||||||
|
//
|
||||||
|
// If the user ever wants to log in using a password
|
||||||
|
// rather than oidc flow, they'll have to request a
|
||||||
|
// password reset, which is fine.
|
||||||
password := uuid.NewString() + uuid.NewString()
|
password := uuid.NewString() + uuid.NewString()
|
||||||
|
|
||||||
// Since this user is created via oidc, which has been set up by the admin, we can assume that the account is already
|
// Since this user is created via OIDC, we can assume
|
||||||
// implicitly approved, and that the email address has already been verified: otherwise, we end up in situations where
|
// that the account should be preapproved, and the email
|
||||||
// the admin first approves the user in OIDC, and then has to approve them again in GoToSocial, which doesn't make sense.
|
// address should be considered as verified already,
|
||||||
|
// since the OIDC login was successful.
|
||||||
//
|
//
|
||||||
// In other words, if a user logs in via OIDC, they should be able to use their account straight away.
|
// If we don't assume this, we end up in a situation
|
||||||
|
// where the admin first adds a user to OIDC, then has
|
||||||
|
// to approve them again in GoToSocial when they log in
|
||||||
|
// there for the first time, which doesn't make sense.
|
||||||
//
|
//
|
||||||
// See: https://github.com/superseriousbusiness/gotosocial/issues/357
|
// In other words, if a user logs in via OIDC, they
|
||||||
requireApproval := false
|
// should be able to use their account straight away.
|
||||||
emailVerified := true
|
var (
|
||||||
|
preApproved = true
|
||||||
|
emailVerified = true
|
||||||
|
)
|
||||||
|
|
||||||
// create the user! this will also create an account and store it in the database so we don't need to do that here
|
// If one of the claimed groups corresponds to one of
|
||||||
user, err := m.db.NewSignup(ctx, extraInfo.Username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, claims.Sub, admin)
|
// the configured admin OIDC groups, create this user
|
||||||
|
// as an admin.
|
||||||
|
admin := adminGroup(claims.Groups)
|
||||||
|
|
||||||
|
// Create the user! This will also create an account and
|
||||||
|
// store it in the database, so we don't need to do that.
|
||||||
|
user, err := m.db.NewSignup(ctx, gtsmodel.NewSignup{
|
||||||
|
Username: extraInfo.Username,
|
||||||
|
Email: claims.Email,
|
||||||
|
Password: password,
|
||||||
|
SignUpIP: ip,
|
||||||
|
AppID: appID,
|
||||||
|
ExternalID: claims.Sub,
|
||||||
|
PreApproved: preApproved,
|
||||||
|
EmailVerified: emailVerified,
|
||||||
|
Admin: admin,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := gtserror.Newf("db error doing new signup: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adminGroup returns true if one of the given OIDC
|
||||||
|
// groups is equal to at least one admin OIDC group.
|
||||||
|
func adminGroup(groups []string) bool {
|
||||||
|
for _, ag := range config.GetOIDCAdminGroups() {
|
||||||
|
for _, g := range groups {
|
||||||
|
if strings.EqualFold(ag, g) {
|
||||||
|
// This is an admin group,
|
||||||
|
// ∴ user is an admin.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is in no admin groups,
|
||||||
|
// ∴ user is not an admin.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@ func validateCreateAccount(form *apimodel.AccountCreateRequest) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validate.NewPassword(form.Password); err != nil {
|
if err := validate.Password(form.Password); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
@ -39,7 +38,7 @@ type Admin interface {
|
||||||
|
|
||||||
// NewSignup creates a new user in the database with the given parameters.
|
// NewSignup creates a new user in the database with the given parameters.
|
||||||
// By the time this function is called, it should be assumed that all the parameters have passed validation!
|
// By the time this function is called, it should be assumed that all the parameters have passed validation!
|
||||||
NewSignup(ctx context.Context, username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, externalID string, admin bool) (*gtsmodel.User, Error)
|
NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (*gtsmodel.User, Error)
|
||||||
|
|
||||||
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
||||||
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
||||||
|
|
|
@ -21,8 +21,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -89,106 +90,134 @@ func (a *adminDB) IsEmailAvailable(ctx context.Context, email string) (bool, db.
|
||||||
return a.conn.NotExists(ctx, q)
|
return a.conn.NotExists(ctx, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adminDB) NewSignup(ctx context.Context, username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, externalID string, admin bool) (*gtsmodel.User, db.Error) {
|
func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (*gtsmodel.User, db.Error) {
|
||||||
key, err := rsa.GenerateKey(rand.Reader, rsaKeyBits)
|
// If something went wrong previously while doing a new
|
||||||
if err != nil {
|
// sign up with this username, we might already have an
|
||||||
log.Errorf(ctx, "error creating new rsa key: %s", err)
|
// account, so check first.
|
||||||
|
account, err := a.state.DB.GetAccountByUsernameDomain(ctx, newSignup.Username, "")
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Real error occurred.
|
||||||
|
err := gtserror.Newf("error checking for existing account: %w", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if something went wrong while creating a user, we might already have an account, so check here first...
|
// If we didn't yet have an account
|
||||||
acct := >smodel.Account{}
|
// with this username, create one now.
|
||||||
if err := a.conn.
|
if account == nil {
|
||||||
NewSelect().
|
uris := uris.GenerateURIsForAccount(newSignup.Username)
|
||||||
Model(acct).
|
|
||||||
Where("? = ?", bun.Ident("account.username"), username).
|
|
||||||
Where("? IS NULL", bun.Ident("account.domain")).
|
|
||||||
Scan(ctx); err != nil {
|
|
||||||
err = a.conn.ProcessError(err)
|
|
||||||
if err != db.ErrNoEntries {
|
|
||||||
log.Errorf(ctx, "error checking for existing account: %s", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have db.ErrNoEntries, we just don't have an
|
|
||||||
// account yet so create one before we proceed
|
|
||||||
accountURIs := uris.GenerateURIsForAccount(username)
|
|
||||||
accountID, err := id.NewRandomULID()
|
accountID, err := id.NewRandomULID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error creating new account id: %w", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
acct = >smodel.Account{
|
privKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBits)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error creating new rsa private key: %w", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account = >smodel.Account{
|
||||||
ID: accountID,
|
ID: accountID,
|
||||||
Username: username,
|
Username: newSignup.Username,
|
||||||
DisplayName: username,
|
DisplayName: newSignup.Username,
|
||||||
Reason: reason,
|
Reason: newSignup.Reason,
|
||||||
Privacy: gtsmodel.VisibilityDefault,
|
Privacy: gtsmodel.VisibilityDefault,
|
||||||
URL: accountURIs.UserURL,
|
URI: uris.UserURI,
|
||||||
PrivateKey: key,
|
URL: uris.UserURL,
|
||||||
PublicKey: &key.PublicKey,
|
InboxURI: uris.InboxURI,
|
||||||
PublicKeyURI: accountURIs.PublicKeyURI,
|
OutboxURI: uris.OutboxURI,
|
||||||
|
FollowingURI: uris.FollowingURI,
|
||||||
|
FollowersURI: uris.FollowersURI,
|
||||||
|
FeaturedCollectionURI: uris.FeaturedCollectionURI,
|
||||||
ActorType: ap.ActorPerson,
|
ActorType: ap.ActorPerson,
|
||||||
URI: accountURIs.UserURI,
|
PrivateKey: privKey,
|
||||||
InboxURI: accountURIs.InboxURI,
|
PublicKey: &privKey.PublicKey,
|
||||||
OutboxURI: accountURIs.OutboxURI,
|
PublicKeyURI: uris.PublicKeyURI,
|
||||||
FollowersURI: accountURIs.FollowersURI,
|
|
||||||
FollowingURI: accountURIs.FollowingURI,
|
|
||||||
FeaturedCollectionURI: accountURIs.FeaturedCollectionURI,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert the new account!
|
// Insert the new account!
|
||||||
if err := a.state.DB.PutAccount(ctx, acct); err != nil {
|
if err := a.state.DB.PutAccount(ctx, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we either created or already had an account by now,
|
// Created or already had an account.
|
||||||
// so proceed with creating a user for that account
|
// Ensure user not already created.
|
||||||
|
user, err := a.state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
if err != nil {
|
// Real error occurred.
|
||||||
return nil, fmt.Errorf("error hashing password: %s", err)
|
err := gtserror.Newf("error checking for existing user: %w", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Pin account to (new)
|
||||||
|
// user before returning.
|
||||||
|
user.Account = account
|
||||||
|
}()
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
// Already had a user for this
|
||||||
|
// account, just return that.
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Had no user for this account, time to create one!
|
||||||
newUserID, err := id.NewRandomULID()
|
newUserID, err := id.NewRandomULID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error creating new user id: %w", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we don't require moderator approval, just pre-approve the user
|
encryptedPassword, err := bcrypt.GenerateFromPassword(
|
||||||
approved := !requireApproval
|
[]byte(newSignup.Password),
|
||||||
u := >smodel.User{
|
bcrypt.DefaultCost,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error hashing password: %w", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user = >smodel.User{
|
||||||
ID: newUserID,
|
ID: newUserID,
|
||||||
AccountID: acct.ID,
|
AccountID: account.ID,
|
||||||
Account: acct,
|
Account: account,
|
||||||
EncryptedPassword: string(pw),
|
EncryptedPassword: string(encryptedPassword),
|
||||||
SignUpIP: signUpIP.To4(),
|
SignUpIP: newSignup.SignUpIP.To4(),
|
||||||
Locale: locale,
|
Locale: newSignup.Locale,
|
||||||
UnconfirmedEmail: email,
|
UnconfirmedEmail: newSignup.Email,
|
||||||
CreatedByApplicationID: appID,
|
CreatedByApplicationID: newSignup.AppID,
|
||||||
Approved: &approved,
|
ExternalID: newSignup.ExternalID,
|
||||||
ExternalID: externalID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if emailVerified {
|
if newSignup.EmailVerified {
|
||||||
u.ConfirmedAt = time.Now()
|
// Mark given email as confirmed.
|
||||||
u.Email = email
|
user.ConfirmedAt = time.Now()
|
||||||
|
user.Email = newSignup.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
if admin {
|
trueBool := func() *bool { t := true; return &t }
|
||||||
admin := true
|
|
||||||
moderator := true
|
if newSignup.Admin {
|
||||||
u.Admin = &admin
|
// Make new user mod + admin.
|
||||||
u.Moderator = &moderator
|
user.Moderator = trueBool()
|
||||||
|
user.Admin = trueBool()
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert the user!
|
if newSignup.PreApproved {
|
||||||
if err := a.state.DB.PutUser(ctx, u); err != nil {
|
// Mark new user as approved.
|
||||||
|
user.Approved = trueBool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the user!
|
||||||
|
if err := a.state.DB.PutUser(ctx, user); err != nil {
|
||||||
|
err := gtserror.Newf("db error inserting user: %w", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error {
|
func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error {
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
|
|
||||||
package gtsmodel
|
package gtsmodel
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// AdminAccountAction models an action taken by an instance administrator on an account.
|
// AdminAccountAction models an action taken by an instance administrator on an account.
|
||||||
type AdminAccountAction struct {
|
type AdminAccountAction struct {
|
||||||
|
@ -45,3 +48,23 @@ const (
|
||||||
// AdminActionSuspend -- the account or application etc has been deleted.
|
// AdminActionSuspend -- the account or application etc has been deleted.
|
||||||
AdminActionSuspend AdminActionType = "suspend"
|
AdminActionSuspend AdminActionType = "suspend"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewSignup models parameters for the creation
|
||||||
|
// of a new user + account on this instance.
|
||||||
|
//
|
||||||
|
// Aside from username, email, and password, it is
|
||||||
|
// fine to use zero values on fields of this struct.
|
||||||
|
type NewSignup struct {
|
||||||
|
Username string // Username of the new account.
|
||||||
|
Email string // Email address of the user.
|
||||||
|
Password string // Plaintext (not yet hashed) password for the user.
|
||||||
|
|
||||||
|
Reason string // Reason given by the user when submitting a sign up request (optional).
|
||||||
|
PreApproved bool // Mark the new user/account as preapproved (optional)
|
||||||
|
SignUpIP net.IP // IP address from which the sign up request occurred (optional).
|
||||||
|
Locale string // Locale code for the new account/user (optional).
|
||||||
|
AppID string // ID of the application used to create this account (optional).
|
||||||
|
EmailVerified bool // Mark submitted email address as already verified (optional).
|
||||||
|
ExternalID string // ID of this user in external OIDC system (optional).
|
||||||
|
Admin bool // Mark new user as an admin user (optional).
|
||||||
|
}
|
||||||
|
|
|
@ -26,61 +26,73 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/oauth2/v4"
|
"github.com/superseriousbusiness/oauth2/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create processes the given form for creating a new account, returning an oauth token for that account if successful.
|
// Create processes the given form for creating a new account,
|
||||||
func (p *Processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
|
// returning an oauth token for that account if successful.
|
||||||
|
//
|
||||||
|
// Fields on the form should have already been validated by the
|
||||||
|
// caller, before this function is called.
|
||||||
|
func (p *Processor) Create(
|
||||||
|
ctx context.Context,
|
||||||
|
appToken oauth2.TokenInfo,
|
||||||
|
app *gtsmodel.Application,
|
||||||
|
form *apimodel.AccountCreateRequest,
|
||||||
|
) (*apimodel.Token, gtserror.WithCode) {
|
||||||
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
|
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
err := fmt.Errorf("db error checking email availability: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if !emailAvailable {
|
if !emailAvailable {
|
||||||
return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", form.Email))
|
err := fmt.Errorf("email address %s is not available", form.Email)
|
||||||
|
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameAvailable, err := p.state.DB.IsUsernameAvailable(ctx, form.Username)
|
usernameAvailable, err := p.state.DB.IsUsernameAvailable(ctx, form.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
err := fmt.Errorf("db error checking username availability: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if !usernameAvailable {
|
if !usernameAvailable {
|
||||||
return nil, gtserror.NewErrorConflict(fmt.Errorf("username %s in use", form.Username))
|
err := fmt.Errorf("username %s is not available", form.Username)
|
||||||
|
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
reasonRequired := config.GetAccountsReasonRequired()
|
// Only store reason if one is required.
|
||||||
approvalRequired := config.GetAccountsApprovalRequired()
|
var reason string
|
||||||
|
if config.GetAccountsReasonRequired() {
|
||||||
// don't store a reason if we don't require one
|
reason = form.Reason
|
||||||
reason := form.Reason
|
|
||||||
if !reasonRequired {
|
|
||||||
reason = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace(ctx, "creating new username and account")
|
user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
|
||||||
user, err := p.state.DB.NewSignup(ctx, form.Username, text.SanitizePlaintext(reason), approvalRequired, form.Email, form.Password, form.IP, form.Locale, application.ID, false, "", false)
|
Username: form.Username,
|
||||||
|
Email: form.Email,
|
||||||
|
Password: form.Password,
|
||||||
|
Reason: text.SanitizePlaintext(reason),
|
||||||
|
PreApproved: !config.GetAccountsApprovalRequired(), // Mark as approved if no approval required.
|
||||||
|
SignUpIP: form.IP,
|
||||||
|
Locale: form.Locale,
|
||||||
|
AppID: app.ID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new signup in the database: %s", err))
|
err := fmt.Errorf("db error creating new signup: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef(ctx, "generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID)
|
// Generate access token *before* doing side effects; we
|
||||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, applicationToken, application.ClientSecret, user.ID)
|
// don't want to process side effects if something borks.
|
||||||
|
accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, appToken, app.ClientSecret, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new access token for user %s: %s", user.ID, err))
|
err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Account == nil {
|
// There are side effects for creating a new account
|
||||||
a, err := p.state.DB.GetAccountByID(ctx, user.AccountID)
|
// (confirmation emails etc), perform these async.
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting new account from the database: %s", err))
|
|
||||||
}
|
|
||||||
user.Account = a
|
|
||||||
}
|
|
||||||
|
|
||||||
// there are side effects for creating a new account (sending confirmation emails etc)
|
|
||||||
// so pass a message to the processor so that it can do it asynchronously
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
|
|
|
@ -28,22 +28,41 @@ import (
|
||||||
|
|
||||||
// PasswordChange processes a password change request for the given user.
|
// PasswordChange processes a password change request for the given user.
|
||||||
func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
|
func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
|
||||||
|
// Ensure provided oldPassword is the correct current password.
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
|
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
|
||||||
|
err := gtserror.Newf("%w", err)
|
||||||
return gtserror.NewErrorUnauthorized(err, "old password was incorrect")
|
return gtserror.NewErrorUnauthorized(err, "old password was incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validate.NewPassword(newPassword); err != nil {
|
// Ensure new password is strong enough.
|
||||||
|
if err := validate.Password(newPassword); err != nil {
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
// Ensure new password is different from old password.
|
||||||
if err != nil {
|
if newPassword == oldPassword {
|
||||||
return gtserror.NewErrorInternalError(err, "error hashing password")
|
const help = "new password cannot be the same as previous password"
|
||||||
|
err := gtserror.New(help)
|
||||||
|
return gtserror.NewErrorBadRequest(err, help)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.EncryptedPassword = string(newPasswordHash)
|
// Hash the new password.
|
||||||
|
encryptedPassword, err := bcrypt.GenerateFromPassword(
|
||||||
|
[]byte(newPassword),
|
||||||
|
bcrypt.DefaultCost,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("%w", err)
|
||||||
|
return gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.state.DB.UpdateUser(ctx, user, "encrypted_password"); err != nil {
|
// Set new password on user.
|
||||||
|
user.EncryptedPassword = string(encryptedPassword)
|
||||||
|
if err := p.state.DB.UpdateUser(
|
||||||
|
ctx, user,
|
||||||
|
"encrypted_password",
|
||||||
|
); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating user: %w", err)
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
|
||||||
user := suite.testUsers["local_account_1"]
|
user := suite.testUsers["local_account_1"]
|
||||||
|
|
||||||
errWithCode := suite.user.PasswordChange(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
|
errWithCode := suite.user.PasswordChange(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
|
||||||
suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password")
|
suite.EqualError(errWithCode, "PasswordChange: crypto/bcrypt: hashedPassword is not the hash of the given password")
|
||||||
suite.Equal(http.StatusUnauthorized, errWithCode.Code())
|
suite.Equal(http.StatusUnauthorized, errWithCode.Code())
|
||||||
suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe())
|
suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe())
|
||||||
|
|
||||||
|
|
|
@ -46,9 +46,9 @@ const (
|
||||||
maximumListTitleLength = 200
|
maximumListTitleLength = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewPassword returns a helpful error if the given password
|
// Password returns a helpful error if the given password
|
||||||
// is too short, too long, or not sufficiently strong.
|
// is too short, too long, or not sufficiently strong.
|
||||||
func NewPassword(password string) error {
|
func Password(password string) error {
|
||||||
// Ensure length is OK first.
|
// Ensure length is OK first.
|
||||||
if pwLen := len(password); pwLen == 0 {
|
if pwLen := len(password); pwLen == 0 {
|
||||||
return errors.New("no password provided / provided password was 0 bytes")
|
return errors.New("no password provided / provided password was 0 bytes")
|
||||||
|
|
|
@ -43,42 +43,42 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() {
|
||||||
strongPassword := "3dX5@Zc%mV*W2MBNEy$@"
|
strongPassword := "3dX5@Zc%mV*W2MBNEy$@"
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
err = validate.NewPassword(empty)
|
err = validate.Password(empty)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("no password provided / provided password was 0 bytes"), err)
|
suite.Equal(errors.New("no password provided / provided password was 0 bytes"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(terriblePassword)
|
err = validate.Password(terriblePassword)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("password is only 62% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"), err)
|
suite.Equal(errors.New("password is only 62% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(weakPassword)
|
err = validate.Password(weakPassword)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("password is only 95% strength, try including more special characters, using numbers or using a longer password"), err)
|
suite.Equal(errors.New("password is only 95% strength, try including more special characters, using numbers or using a longer password"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(shortPassword)
|
err = validate.Password(shortPassword)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("password is only 39% strength, try including more special characters or using a longer password"), err)
|
suite.Equal(errors.New("password is only 39% strength, try including more special characters or using a longer password"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(specialPassword)
|
err = validate.Password(specialPassword)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("password is only 53% strength, try including more special characters or using a longer password"), err)
|
suite.Equal(errors.New("password is only 53% strength, try including more special characters or using a longer password"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(longPassword)
|
err = validate.Password(longPassword)
|
||||||
if suite.NoError(err) {
|
if suite.NoError(err) {
|
||||||
suite.Equal(nil, err)
|
suite.Equal(nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(tooLong)
|
err = validate.Password(tooLong)
|
||||||
if suite.Error(err) {
|
if suite.Error(err) {
|
||||||
suite.Equal(errors.New("password should be no more than 72 bytes, provided password was 571 bytes"), err)
|
suite.Equal(errors.New("password should be no more than 72 bytes, provided password was 571 bytes"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.NewPassword(strongPassword)
|
err = validate.Password(strongPassword)
|
||||||
if suite.NoError(err) {
|
if suite.NoError(err) {
|
||||||
suite.Equal(nil, err)
|
suite.Equal(nil, err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue