Admin cli (#29)

Now you can use the CLI tool to:

* Create a new account with the given username, email address and password (which will be hashed of course).
* Confirm the account's so that it can log in and post.
* Promote the account to admin.
* Demote the account from admin.
* Disable the account.
* Suspend the account.
This commit is contained in:
Tobi Smethurst 2021-05-22 14:26:45 +02:00 committed by GitHub
parent 0df2e18cc0
commit 43c3a47773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 364 additions and 2 deletions

View file

@ -24,6 +24,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/action" "github.com/superseriousbusiness/gotosocial/internal/action"
"github.com/superseriousbusiness/gotosocial/internal/clitools/admin/account"
"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/gotosocial" "github.com/superseriousbusiness/gotosocial/internal/gotosocial"
@ -263,6 +264,104 @@ func main() {
}, },
}, },
}, },
{
Name: "admin",
Usage: "gotosocial admin-related tasks",
Subcommands: []*cli.Command{
{
Name: "account",
Usage: "admin commands related to accounts",
Subcommands: []*cli.Command{
{
Name: "create",
Usage: "create a new account",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
&cli.StringFlag{
Name: config.EmailFlag,
Usage: config.EmailUsage,
},
&cli.StringFlag{
Name: config.PasswordFlag,
Usage: config.PasswordUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Create)
},
},
{
Name: "confirm",
Usage: "confirm an existing account manually, thereby skipping email confirmation",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Confirm)
},
},
{
Name: "promote",
Usage: "promote an account to admin",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Promote)
},
},
{
Name: "demote",
Usage: "demote an account from admin to normal user",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Demote)
},
},
{
Name: "disable",
Usage: "prevent an account from signing in or posting etc, but don't delete anything",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Disable)
},
},
{
Name: "suspend",
Usage: "completely remove an account and all of its posts, media, etc",
Flags: []cli.Flag{
&cli.StringFlag{
Name: config.UsernameFlag,
Usage: config.UsernameUsage,
},
},
Action: func(c *cli.Context) error {
return runAction(c, account.Suspend)
},
},
},
},
},
},
{ {
Name: "db", Name: "db",
Usage: "database-related tasks and utils", Usage: "database-related tasks and utils",
@ -308,7 +407,9 @@ func runAction(c *cli.Context, a action.GTSAction) error {
return fmt.Errorf("error creating config: %s", err) return fmt.Errorf("error creating config: %s", err)
} }
// ... and the flags set on the *cli.Context by urfave // ... and the flags set on the *cli.Context by urfave
conf.ParseCLIFlags(c) if err := conf.ParseCLIFlags(c); err != nil {
return fmt.Errorf("error parsing config: %s", err)
}
// create a logger with the log level, formatting, and output splitter already set // create a logger with the log level, formatting, and output splitter already set
log, err := log.New(conf.LogLevel) log, err := log.New(conf.LogLevel)

View file

@ -0,0 +1,209 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package account
import (
"context"
"errors"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/action"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/pg"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// Create creates a new account in the database using the provided flags.
var Create action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbConn, err := pg.NewPostgresService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
username, ok := c.AccountCLIFlags[config.UsernameFlag]
if !ok {
return errors.New("no username set")
}
if err := util.ValidateUsername(username); err != nil {
return err
}
email, ok := c.AccountCLIFlags[config.EmailFlag]
if !ok {
return errors.New("no email set")
}
if err := util.ValidateEmail(email); err != nil {
return err
}
password, ok := c.AccountCLIFlags[config.PasswordFlag]
if !ok {
return errors.New("no password set")
}
if err := util.ValidateNewPassword(password); err != nil {
return err
}
_, err = dbConn.NewSignup(username, "", false, email, password, nil, "", "")
if err != nil {
return err
}
return dbConn.Stop(ctx)
}
// 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, c *config.Config, log *logrus.Logger) error {
dbConn, err := pg.NewPostgresService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
username, ok := c.AccountCLIFlags[config.UsernameFlag]
if !ok {
return errors.New("no username set")
}
if err := util.ValidateUsername(username); err != nil {
return err
}
a := &gtsmodel.Account{}
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
return err
}
u := &gtsmodel.User{}
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
return err
}
u.Approved = true
u.Email = u.UnconfirmedEmail
u.ConfirmedAt = time.Now()
if err := dbConn.UpdateByID(u.ID, u); err != nil {
return err
}
return dbConn.Stop(ctx)
}
// Promote sets a user to admin.
var Promote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbConn, err := pg.NewPostgresService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
username, ok := c.AccountCLIFlags[config.UsernameFlag]
if !ok {
return errors.New("no username set")
}
if err := util.ValidateUsername(username); err != nil {
return err
}
a := &gtsmodel.Account{}
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
return err
}
u := &gtsmodel.User{}
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
return err
}
u.Admin = true
if err := dbConn.UpdateByID(u.ID, u); err != nil {
return err
}
return dbConn.Stop(ctx)
}
// Demote sets admin on a user to false.
var Demote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbConn, err := pg.NewPostgresService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
username, ok := c.AccountCLIFlags[config.UsernameFlag]
if !ok {
return errors.New("no username set")
}
if err := util.ValidateUsername(username); err != nil {
return err
}
a := &gtsmodel.Account{}
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
return err
}
u := &gtsmodel.User{}
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
return err
}
u.Admin = false
if err := dbConn.UpdateByID(u.ID, u); err != nil {
return err
}
return dbConn.Stop(ctx)
}
// Disable sets Disabled to true on a user.
var Disable action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbConn, err := pg.NewPostgresService(ctx, c, log)
if err != nil {
return fmt.Errorf("error creating dbservice: %s", err)
}
username, ok := c.AccountCLIFlags[config.UsernameFlag]
if !ok {
return errors.New("no username set")
}
if err := util.ValidateUsername(username); err != nil {
return err
}
a := &gtsmodel.Account{}
if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
return err
}
u := &gtsmodel.User{}
if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
return err
}
u.Disabled = true
if err := dbConn.UpdateByID(u.ID, u); err != nil {
return err
}
return dbConn.Stop(ctx)
}
var Suspend action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
// TODO
return nil
}

View file

@ -19,14 +19,31 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
const (
UsernameFlag = "username"
UsernameUsage = "the username to create/delete/etc"
EmailFlag = "email"
EmailUsage = "the email address of this account"
PasswordFlag = "password"
PasswordUsage = "the password to set for this account"
)
// Config pulls together all the configuration needed to run gotosocial // Config pulls together all the configuration needed to run gotosocial
type Config struct { type Config struct {
/*
Parseable from .yaml configuration file.
For long-running commands (server start etc).
*/
LogLevel string `yaml:"logLevel"` LogLevel string `yaml:"logLevel"`
ApplicationName string `yaml:"applicationName"` ApplicationName string `yaml:"applicationName"`
Host string `yaml:"host"` Host string `yaml:"host"`
@ -38,6 +55,12 @@ type Config struct {
StorageConfig *StorageConfig `yaml:"storage"` StorageConfig *StorageConfig `yaml:"storage"`
StatusesConfig *StatusesConfig `yaml:"statuses"` StatusesConfig *StatusesConfig `yaml:"statuses"`
LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"` LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"`
/*
Not parsed from .yaml configuration file.
For short running commands (admin CLI tools etc).
*/
AccountCLIFlags map[string]string
} }
// FromFile returns a new config from a file, or an error if something goes amiss. // FromFile returns a new config from a file, or an error if something goes amiss.
@ -62,6 +85,7 @@ func Empty() *Config {
StorageConfig: &StorageConfig{}, StorageConfig: &StorageConfig{},
StatusesConfig: &StatusesConfig{}, StatusesConfig: &StatusesConfig{},
LetsEncryptConfig: &LetsEncryptConfig{}, LetsEncryptConfig: &LetsEncryptConfig{},
AccountCLIFlags: make(map[string]string),
} }
} }
@ -81,7 +105,7 @@ func loadFromFile(path string) (*Config, error) {
} }
// ParseCLIFlags sets flags on the config using the provided Flags object // ParseCLIFlags sets flags on the config using the provided Flags object
func (c *Config) ParseCLIFlags(f KeyedFlags) { func (c *Config) ParseCLIFlags(f KeyedFlags) error {
fn := GetFlagNames() fn := GetFlagNames()
// For all of these flags, we only want to set them on the config if: // For all of these flags, we only want to set them on the config if:
@ -104,10 +128,16 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
if c.Host == "" || f.IsSet(fn.Host) { if c.Host == "" || f.IsSet(fn.Host) {
c.Host = f.String(fn.Host) c.Host = f.String(fn.Host)
} }
if c.Host == "" {
return errors.New("host was not set")
}
if c.Protocol == "" || f.IsSet(fn.Protocol) { if c.Protocol == "" || f.IsSet(fn.Protocol) {
c.Protocol = f.String(fn.Protocol) c.Protocol = f.String(fn.Protocol)
} }
if c.Protocol == "" {
return errors.New("protocol was not set")
}
// db flags // db flags
if c.DBConfig.Type == "" || f.IsSet(fn.DbType) { if c.DBConfig.Type == "" || f.IsSet(fn.DbType) {
@ -215,6 +245,15 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) { if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) {
c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress) c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress)
} }
// command-specific flags
// admin account CLI flags
c.AccountCLIFlags[UsernameFlag] = f.String(UsernameFlag)
c.AccountCLIFlags[EmailFlag] = f.String(EmailFlag)
c.AccountCLIFlags[PasswordFlag] = f.String(PasswordFlag)
return nil
} }
// KeyedFlags is a wrapper for any type that can store keyed flags and give them back. // KeyedFlags is a wrapper for any type that can store keyed flags and give them back.

View file

@ -54,9 +54,22 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap
return nil, NewErrorNotFound(err) return nil, NewErrorNotFound(err)
} }
originAccount := &gtsmodel.Account{}
if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
return nil, NewErrorInternalError(err)
}
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
return nil, NewErrorInternalError(err)
}
p.fromClientAPI <- gtsmodel.FromClientAPI{ p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsAccept, APActivityType: gtsmodel.ActivityStreamsAccept,
GTSModel: follow, GTSModel: follow,
OriginAccount: originAccount,
TargetAccount: targetAccount,
} }
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)