mirror of
				https://github.com/superseriousbusiness/gotosocial
				synced 2025-06-05 21:59:39 +02:00 
			
		
		
		
	[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:
		| @@ -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) | ||||||
| 			ID:                    accountID, |  | ||||||
| 			Username:              username, |  | ||||||
| 			DisplayName:           username, |  | ||||||
| 			Reason:                reason, |  | ||||||
| 			Privacy:               gtsmodel.VisibilityDefault, |  | ||||||
| 			URL:                   accountURIs.UserURL, |  | ||||||
| 			PrivateKey:            key, |  | ||||||
| 			PublicKey:             &key.PublicKey, |  | ||||||
| 			PublicKeyURI:          accountURIs.PublicKeyURI, |  | ||||||
| 			ActorType:             ap.ActorPerson, |  | ||||||
| 			URI:                   accountURIs.UserURI, |  | ||||||
| 			InboxURI:              accountURIs.InboxURI, |  | ||||||
| 			OutboxURI:             accountURIs.OutboxURI, |  | ||||||
| 			FollowersURI:          accountURIs.FollowersURI, |  | ||||||
| 			FollowingURI:          accountURIs.FollowingURI, |  | ||||||
| 			FeaturedCollectionURI: accountURIs.FeaturedCollectionURI, |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// insert the new account! |  | ||||||
| 		if err := a.state.DB.PutAccount(ctx, acct); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// we either created or already had an account by now, |  | ||||||
| 	// so proceed with creating a user for that account |  | ||||||
|  |  | ||||||
| 	pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 		return nil, fmt.Errorf("error hashing password: %s", err) | 			err := gtserror.Newf("error creating new rsa private key: %w", err) | ||||||
|  | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		account = >smodel.Account{ | ||||||
|  | 			ID:                    accountID, | ||||||
|  | 			Username:              newSignup.Username, | ||||||
|  | 			DisplayName:           newSignup.Username, | ||||||
|  | 			Reason:                newSignup.Reason, | ||||||
|  | 			Privacy:               gtsmodel.VisibilityDefault, | ||||||
|  | 			URI:                   uris.UserURI, | ||||||
|  | 			URL:                   uris.UserURL, | ||||||
|  | 			InboxURI:              uris.InboxURI, | ||||||
|  | 			OutboxURI:             uris.OutboxURI, | ||||||
|  | 			FollowingURI:          uris.FollowingURI, | ||||||
|  | 			FollowersURI:          uris.FollowersURI, | ||||||
|  | 			FeaturedCollectionURI: uris.FeaturedCollectionURI, | ||||||
|  | 			ActorType:             ap.ActorPerson, | ||||||
|  | 			PrivateKey:            privKey, | ||||||
|  | 			PublicKey:             &privKey.PublicKey, | ||||||
|  | 			PublicKeyURI:          uris.PublicKeyURI, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Insert the new account! | ||||||
|  | 		if err := a.state.DB.PutAccount(ctx, account); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Created or already had an account. | ||||||
|  | 	// Ensure user not already created. | ||||||
|  | 	user, err := a.state.DB.GetUserByAccountID(ctx, account.ID) | ||||||
|  | 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		// Real error occurred. | ||||||
|  | 		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) | ||||||
| 	} | 	} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user