[feature] 2fa management via CLI (#4368)

Adds 2FA management to the admin CLI. Also does some CLI refactoring so the functions we pass around are exported functions instead of changeable global variables.

closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4320

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4368
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-08-13 12:24:40 +02:00 committed by kim
commit 7f8cb204cd
15 changed files with 224 additions and 113 deletions

View file

@ -29,12 +29,25 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/db/bundb"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
userprocessor "code.superseriousbusiness.org/gotosocial/internal/processing/user"
"code.superseriousbusiness.org/gotosocial/internal/state"
"code.superseriousbusiness.org/gotosocial/internal/util"
"code.superseriousbusiness.org/gotosocial/internal/validate"
"golang.org/x/crypto/bcrypt"
)
var (
// check function conformance
_ action.GTSAction = Create
_ action.GTSAction = List
_ action.GTSAction = Confirm
_ action.GTSAction = Promote
_ action.GTSAction = Demote
_ action.GTSAction = Enable
_ action.GTSAction = Disable
_ action.GTSAction = Password
)
func initState(ctx context.Context) (*state.State, error) {
var state state.State
state.Caches.Init()
@ -61,7 +74,7 @@ func stopState(state *state.State) error {
// Create creates a new account and user
// in the database using the provided flags.
var Create action.GTSAction = func(ctx context.Context) error {
func Create(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -118,7 +131,7 @@ var Create action.GTSAction = func(ctx context.Context) error {
}
// List returns all existing local accounts.
var List action.GTSAction = func(ctx context.Context) error {
func List(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -156,7 +169,7 @@ var List action.GTSAction = func(ctx context.Context) error {
// 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 {
func Confirm(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -198,7 +211,7 @@ var Confirm action.GTSAction = func(ctx context.Context) error {
}
// Promote sets admin + moderator flags on a user to true.
var Promote action.GTSAction = func(ctx context.Context) error {
func Promote(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -235,7 +248,7 @@ var Promote action.GTSAction = func(ctx context.Context) error {
}
// Demote sets admin + moderator flags on a user to false.
var Demote action.GTSAction = func(ctx context.Context) error {
func Demote(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -272,7 +285,7 @@ var Demote action.GTSAction = func(ctx context.Context) error {
}
// Disable sets Disabled to true on a user.
var Disable action.GTSAction = func(ctx context.Context) error {
func Disable(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -308,7 +321,7 @@ var Disable action.GTSAction = func(ctx context.Context) error {
}
// Enable sets Disabled to false on a user.
var Enable action.GTSAction = func(ctx context.Context) error {
func Enable(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -344,7 +357,7 @@ var Enable action.GTSAction = func(ctx context.Context) error {
}
// Password sets the password of target account.
var Password action.GTSAction = func(ctx context.Context) error {
func Password(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
@ -389,3 +402,41 @@ var Password action.GTSAction = func(ctx context.Context) error {
"encrypted_password",
)
}
// Disable2FA disables 2FA for target account.
func Disable2FA(ctx context.Context) error {
state, err := initState(ctx)
if err != nil {
return err
}
defer func() {
// Ensure state gets stopped on return.
if err := stopState(state); err != nil {
log.Error(ctx, err)
}
}()
username := config.GetAdminAccountUsername()
if err := validate.Username(username); err != nil {
return err
}
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
return err
}
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
if err != nil {
return err
}
err = userprocessor.TwoFactorDisable(ctx, state, user)
if err != nil {
return err
}
fmt.Printf("2fa disabled\n")
return nil
}

View file

@ -35,6 +35,10 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/state"
)
// check function conformance.
var _ action.GTSAction = ListAttachments
var _ action.GTSAction = ListEmojis
type list struct {
dbService db.DB
state *state.State
@ -155,7 +159,7 @@ func (l *list) shutdown() error {
}
// ListAttachments lists local, remote, or all attachment paths.
var ListAttachments action.GTSAction = func(ctx context.Context) error {
func ListAttachments(ctx context.Context) error {
list, err := setupList(ctx)
if err != nil {
return err
@ -214,7 +218,7 @@ var ListAttachments action.GTSAction = func(ctx context.Context) error {
}
// ListEmojis lists local, remote, or all emoji filepaths.
var ListEmojis action.GTSAction = func(ctx context.Context) error {
func ListEmojis(ctx context.Context) error {
list, err := setupList(ctx)
if err != nil {
return err

View file

@ -26,8 +26,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/log"
)
// check function conformance.
var _ action.GTSAction = All
// All performs all media clean actions
var All action.GTSAction = func(ctx context.Context) error {
func All(ctx context.Context) error {
// Setup pruning utilities.
prune, err := setupPrune(ctx)
if err != nil {

View file

@ -26,8 +26,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/log"
)
// check function conformance.
var _ action.GTSAction = Orphaned
// Orphaned prunes orphaned media from storage.
var Orphaned action.GTSAction = func(ctx context.Context) error {
func Orphaned(ctx context.Context) error {
// Setup pruning utilities.
prune, err := setupPrune(ctx)
if err != nil {

View file

@ -27,8 +27,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/log"
)
// check function conformance.
var _ action.GTSAction = Remote
// Remote prunes old and/or unused remote media.
var Remote action.GTSAction = func(ctx context.Context) error {
func Remote(ctx context.Context) error {
// Setup pruning utilities.
prune, err := setupPrune(ctx)
if err != nil {

View file

@ -29,8 +29,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/trans"
)
// check function conformance.
var _ action.GTSAction = Export
// Export exports info from the database into a file
var Export action.GTSAction = func(ctx context.Context) error {
func Export(ctx context.Context) error {
var state state.State
// Only set state DB connection.

View file

@ -29,8 +29,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/trans"
)
// check function conformance.
var _ action.GTSAction = Import
// Import imports info from a file into the database
var Import action.GTSAction = func(ctx context.Context) error {
func Import(ctx context.Context) error {
var state state.State
// Only set state DB connection.

View file

@ -26,8 +26,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/config"
)
// check function conformance.
var _ action.GTSAction = Config
// Config just prints the collated config out to stdout as json.
var Config action.GTSAction = func(ctx context.Context) (err error) {
func Config(ctx context.Context) (err error) {
var raw map[string]interface{}
// Marshal configuration to a raw JSON map

View file

@ -27,8 +27,11 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/state"
)
// check function conformance.
var _ action.GTSAction = Run
// Run will initialize the database, running any available migrations.
var Run action.GTSAction = func(ctx context.Context) error {
func Run(ctx context.Context) error {
var state state.State
defer func() {

View file

@ -70,9 +70,13 @@ import (
"go.uber.org/automaxprocs/maxprocs"
)
// check function conformance.
var _ action.GTSAction = Maintenance
var _ action.GTSAction = Start
// Maintenance starts and creates a GoToSocial server
// in maintenance mode (returns 503 for most requests).
var Maintenance action.GTSAction = func(ctx context.Context) error {
func Maintenance(ctx context.Context) error {
route, err := router.New(ctx)
if err != nil {
return fmt.Errorf("error creating maintenance router: %w", err)
@ -101,7 +105,7 @@ var Maintenance action.GTSAction = func(ctx context.Context) error {
}
// Start creates and starts a gotosocial server
var Start action.GTSAction = func(ctx context.Context) error {
func Start(ctx context.Context) error {
// Set GOMAXPROCS / GOMEMLIMIT
// to match container limits.
setLimits(ctx)

View file

@ -19,8 +19,15 @@
package testrig
import "code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action"
import (
"context"
"code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action"
)
// check function conformance.
var _ action.GTSAction = Start
// Start creates and starts a gotosocial testrig server.
// This is only enabled in debug builds, else is nil.
var Start action.GTSAction
func Start(context.Context) error { return nil }

View file

@ -50,9 +50,12 @@ import (
"github.com/gin-gonic/gin"
)
// check function conformance.
var _ action.GTSAction = Start
// Start creates and starts a gotosocial testrig server.
// This is only enabled in debug builds, else is nil.
var Start action.GTSAction = func(ctx context.Context) error {
func Start(ctx context.Context) error {
testrig.InitTestConfig()
testrig.InitTestLog()

View file

@ -146,6 +146,19 @@ func adminCommands() *cobra.Command {
config.AddAdminAccountPassword(adminAccountPasswordCmd)
adminAccountCmd.AddCommand(adminAccountPasswordCmd)
adminAccountDisable2FACmd := &cobra.Command{
Use: "disable-2fa",
Short: "disable 2fa for the given local account",
PreRunE: func(cmd *cobra.Command, args []string) error {
return preRun(preRunArgs{cmd: cmd})
},
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd.Context(), account.Disable2FA)
},
}
config.AddAdminAccount(adminAccountDisable2FACmd)
adminAccountCmd.AddCommand(adminAccountDisable2FACmd)
adminCmd.AddCommand(adminAccountCmd)
/*

View file

@ -20,11 +20,12 @@ package main
import (
"code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action/testrig"
"code.superseriousbusiness.org/gotosocial/internal/config"
"codeberg.org/gruf/go-debug"
"github.com/spf13/cobra"
)
func testrigCommands() *cobra.Command {
if testrig.Start != nil {
if debug.DEBUG {
testrigCmd := &cobra.Command{
Use: "testrig",
Short: "gotosocial testrig-related tasks",