3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-02-20 15:40:51 +01:00

Merge pull request #1361 from slingamn/suspend.5

fix #1274
This commit is contained in:
Shivaram Lingamneni 2020-10-28 08:12:46 -07:00 committed by GitHub
commit 2bb2cab6c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 372 additions and 147 deletions

View File

@ -40,8 +40,9 @@ const (
keyAccountChannels = "account.channels %s" // channels registered to the account keyAccountChannels = "account.channels %s" // channels registered to the account
keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined
keyAccountLastSeen = "account.lastseen %s" keyAccountLastSeen = "account.lastseen %s"
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
keyAccountRealname = "account.realname %s" // client realname stored as string keyAccountRealname = "account.realname %s" // client realname stored as string
keyAccountSuspended = "account.suspended %s" // client realname stored as string
maxCertfpsPerAccount = 5 maxCertfpsPerAccount = 5
) )
@ -117,7 +118,7 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) {
for _, accountName := range accounts { for _, accountName := range accounts {
account, err := am.LoadAccount(accountName) account, err := am.LoadAccount(accountName)
if err == nil && account.Verified && if err == nil && (account.Verified && account.Suspended == nil) &&
persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) { persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
am.server.AddAlwaysOnClient( am.server.AddAlwaysOnClient(
account, account,
@ -1035,6 +1036,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
if !account.Verified { if !account.Verified {
err = errAccountUnverified err = errAccountUnverified
return return
} else if account.Suspended != nil {
err = errAccountSuspended
return
} }
switch account.Credentials.Version { switch account.Credentials.Version {
@ -1230,6 +1234,15 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName str
am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error()) am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
} }
} }
if raw.Suspended != "" {
sus := new(AccountSuspension)
e := json.Unmarshal([]byte(raw.Suspended), sus)
if e != nil {
am.server.logger.Error("internal", "corrupt suspension data", result.Name, e.Error())
} else {
result.Suspended = sus
}
}
return return
} }
@ -1243,6 +1256,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
_, e := tx.Get(accountKey) _, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound { if e == buntdb.ErrNotFound {
@ -1257,6 +1271,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
result.AdditionalNicks, _ = tx.Get(nicksKey) result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey) result.VHost, _ = tx.Get(vhostKey)
result.Settings, _ = tx.Get(settingsKey) result.Settings, _ = tx.Get(settingsKey)
result.Suspended, _ = tx.Get(suspendedKey)
if _, e = tx.Get(verifiedKey); e == nil { if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true result.Verified = true
@ -1265,20 +1280,44 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
return return
} }
func (am *AccountManager) Suspend(accountName string) (err error) { type AccountSuspension struct {
AccountName string `json:"AccountName,omitempty"`
TimeCreated time.Time
Duration time.Duration
OperName string
Reason string
}
func (am *AccountManager) Suspend(accountName string, duration time.Duration, operName, reason string) (err error) {
account, err := CasefoldName(accountName) account, err := CasefoldName(accountName)
if err != nil { if err != nil {
return errAccountDoesNotExist return errAccountDoesNotExist
} }
suspension := AccountSuspension{
TimeCreated: time.Now().UTC(),
Duration: duration,
OperName: operName,
Reason: reason,
}
suspensionStr, err := json.Marshal(suspension)
if err != nil {
am.server.logger.Error("internal", "suspension json unserializable", err.Error())
return errAccountDoesNotExist
}
existsKey := fmt.Sprintf(keyAccountExists, account) existsKey := fmt.Sprintf(keyAccountExists, account)
verifiedKey := fmt.Sprintf(keyAccountVerified, account) suspensionKey := fmt.Sprintf(keyAccountSuspended, account)
var setOptions *buntdb.SetOptions
if duration != time.Duration(0) {
setOptions = &buntdb.SetOptions{Expires: true, TTL: duration}
}
err = am.server.store.Update(func(tx *buntdb.Tx) error { err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey) _, err := tx.Get(existsKey)
if err != nil { if err != nil {
return errAccountDoesNotExist return errAccountDoesNotExist
} }
_, err = tx.Delete(verifiedKey) _, _, err = tx.Set(suspensionKey, string(suspensionStr), setOptions)
return err return err
}) })
@ -1293,7 +1332,13 @@ func (am *AccountManager) Suspend(accountName string) (err error) {
delete(am.accountToClients, account) delete(am.accountToClients, account)
am.Unlock() am.Unlock()
am.killClients(clients) // kill clients, sending them the reason
suspension.AccountName = accountName
for _, client := range clients {
client.Logout()
client.Quit(suspensionToString(client, suspension), nil)
client.destroy(nil)
}
return nil return nil
} }
@ -1312,20 +1357,53 @@ func (am *AccountManager) Unsuspend(account string) (err error) {
} }
existsKey := fmt.Sprintf(keyAccountExists, cfaccount) existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount) suspensionKey := fmt.Sprintf(keyAccountSuspended, account)
err = am.server.store.Update(func(tx *buntdb.Tx) error { err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey) _, err := tx.Get(existsKey)
if err != nil { if err != nil {
return errAccountDoesNotExist return errAccountDoesNotExist
} }
tx.Set(verifiedKey, "1", nil) _, err = tx.Delete(suspensionKey)
if err != nil {
return errNoop
}
return nil return nil
}) })
if err != nil { return err
return errAccountDoesNotExist }
func (am *AccountManager) ListSuspended() (result []AccountSuspension) {
var names []string
var raw []string
prefix := fmt.Sprintf(keyAccountSuspended, "")
am.server.store.View(func(tx *buntdb.Tx) error {
err := tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
raw = append(raw, value)
cfname := strings.TrimPrefix(key, prefix)
name, _ := tx.Get(fmt.Sprintf(keyAccountName, cfname))
names = append(names, name)
return true
})
return err
})
result = make([]AccountSuspension, 0, len(raw))
for i := 0; i < len(raw); i++ {
var sus AccountSuspension
err := json.Unmarshal([]byte(raw[i]), &sus)
if err != nil {
am.server.logger.Error("internal", "corrupt data for suspension", names[i], err.Error())
continue
}
sus.AccountName = names[i]
result = append(result, sus)
} }
return nil return
} }
func (am *AccountManager) Unregister(account string, erase bool) error { func (am *AccountManager) Unregister(account string, erase bool) error {
@ -1351,6 +1429,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount) unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount) modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount) realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
var clients []*Client var clients []*Client
defer func() { defer func() {
@ -1410,6 +1489,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
tx.Delete(lastSeenKey) tx.Delete(lastSeenKey)
tx.Delete(modesKey) tx.Delete(modesKey)
tx.Delete(realnameKey) tx.Delete(realnameKey)
tx.Delete(suspendedKey)
return nil return nil
}) })
@ -1491,6 +1571,9 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
} else if !clientAccount.Verified { } else if !clientAccount.Verified {
err = errAccountUnverified err = errAccountUnverified
return return
} else if clientAccount.Suspended != nil {
err = errAccountSuspended
return
} }
// TODO(#1109) clean this check up? // TODO(#1109) clean this check up?
if client.registered { if client.registered {
@ -1882,6 +1965,7 @@ type ClientAccount struct {
RegisteredAt time.Time RegisteredAt time.Time
Credentials AccountCredentials Credentials AccountCredentials
Verified bool Verified bool
Suspended *AccountSuspension
AdditionalNicks []string AdditionalNicks []string
VHost VHostInfo VHost VHostInfo
Settings AccountSettings Settings AccountSettings
@ -1897,4 +1981,5 @@ type rawClientAccount struct {
AdditionalNicks string AdditionalNicks string
VHost string VHost string
Settings string Settings string
Suspended string
} }

View File

@ -24,7 +24,7 @@ const (
// 'version' of the database schema // 'version' of the database schema
keySchemaVersion = "db.version" keySchemaVersion = "db.version"
// latest schema of the db // latest schema of the db
latestDbSchema = "17" latestDbSchema = 18
keyCloakSecret = "crypto.cloak_secret" keyCloakSecret = "crypto.cloak_secret"
) )
@ -32,14 +32,11 @@ const (
type SchemaChanger func(*Config, *buntdb.Tx) error type SchemaChanger func(*Config, *buntdb.Tx) error
type SchemaChange struct { type SchemaChange struct {
InitialVersion string // the change will take this version InitialVersion int // the change will take this version
TargetVersion string // and transform it into this version TargetVersion int // and transform it into this version
Changer SchemaChanger Changer SchemaChanger
} }
// maps an initial version to a schema change capable of upgrading it
var schemaChanges map[string]SchemaChange
func checkDBReadyForInit(path string) error { func checkDBReadyForInit(path string) error {
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { if err == nil {
@ -72,7 +69,7 @@ func initializeDB(path string) error {
err = store.Update(func(tx *buntdb.Tx) error { err = store.Update(func(tx *buntdb.Tx) error {
// set schema version // set schema version
tx.Set(keySchemaVersion, latestDbSchema, nil) tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil)
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
return nil return nil
}) })
@ -100,9 +97,12 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
}() }()
// read the current version string // read the current version string
var version string var version int
err = db.View(func(tx *buntdb.Tx) error { err = db.View(func(tx *buntdb.Tx) (err error) {
version, err = tx.Get(keySchemaVersion) vStr, err := tx.Get(keySchemaVersion)
if err == nil {
version, err = strconv.Atoi(vStr)
}
return err return err
}) })
if err != nil { if err != nil {
@ -130,11 +130,11 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
} }
} }
func performAutoUpgrade(currentVersion string, config *Config) (err error) { func performAutoUpgrade(currentVersion int, config *Config) (err error) {
path := config.Datastore.Path path := config.Datastore.Path
log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema) log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z") timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp) backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp)
log.Printf("making a backup of current database at %s\n", backupPath) log.Printf("making a backup of current database at %s\n", backupPath)
err = utils.CopyFile(path, backupPath) err = utils.CopyFile(path, backupPath)
if err != nil { if err != nil {
@ -164,29 +164,30 @@ func UpgradeDB(config *Config) (err error) {
} }
defer store.Close() defer store.Close()
var version string var version int
err = store.Update(func(tx *buntdb.Tx) error { err = store.Update(func(tx *buntdb.Tx) error {
for { for {
version, _ = tx.Get(keySchemaVersion) vStr, _ := tx.Get(keySchemaVersion)
change, schemaNeedsChange := schemaChanges[version] version, _ = strconv.Atoi(vStr)
if !schemaNeedsChange { if version == latestDbSchema {
if version == latestDbSchema { // success!
// success! break
break }
} change, ok := getSchemaChange(version)
if !ok {
// unable to upgrade to the desired version, roll back // unable to upgrade to the desired version, roll back
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema} return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
} }
log.Println("attempting to update schema from version " + version) log.Printf("attempting to update schema from version %d\n", version)
err := change.Changer(config, tx) err := change.Changer(config, tx)
if err != nil { if err != nil {
return err return err
} }
_, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil) _, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
if err != nil { if err != nil {
return err return err
} }
log.Println("successfully updated schema to version " + change.TargetVersion) log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
} }
return nil return nil
}) })
@ -853,93 +854,148 @@ func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
return nil return nil
} }
func init() { // #1274: we used to suspend accounts by deleting their "verified" key,
allChanges := []SchemaChange{ // now we save some metadata under a new key
{ func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error {
InitialVersion: "1", now := time.Now().UTC()
TargetVersion: "2",
Changer: schemaChangeV1toV2, exists := "account.exists "
}, suspended := "account.suspended "
{ verif := "account.verified "
InitialVersion: "2", verifCode := "account.verificationcode "
TargetVersion: "3",
Changer: schemaChangeV2ToV3, var accounts []string
},
{ tx.AscendGreaterOrEqual("", exists, func(key, value string) bool {
InitialVersion: "3", if !strings.HasPrefix(key, exists) {
TargetVersion: "4", return false
Changer: schemaChangeV3ToV4, }
}, account := strings.TrimPrefix(key, exists)
{ _, verifiedErr := tx.Get(verif + account)
InitialVersion: "4", _, verifCodeErr := tx.Get(verifCode + account)
TargetVersion: "5", if verifiedErr != nil && verifCodeErr != nil {
Changer: schemaChangeV4ToV5, // verified key not present, but there's no code either,
}, // this is a suspension
{ accounts = append(accounts, account)
InitialVersion: "5", }
TargetVersion: "6", return true
Changer: schemaChangeV5ToV6, })
},
{ type accountSuspensionV18 struct {
InitialVersion: "6", TimeCreated time.Time
TargetVersion: "7", Duration time.Duration
Changer: schemaChangeV6ToV7, OperName string
}, Reason string
{
InitialVersion: "7",
TargetVersion: "8",
Changer: schemaChangeV7ToV8,
},
{
InitialVersion: "8",
TargetVersion: "9",
Changer: schemaChangeV8ToV9,
},
{
InitialVersion: "9",
TargetVersion: "10",
Changer: schemaChangeV9ToV10,
},
{
InitialVersion: "10",
TargetVersion: "11",
Changer: schemaChangeV10ToV11,
},
{
InitialVersion: "11",
TargetVersion: "12",
Changer: schemaChangeV11ToV12,
},
{
InitialVersion: "12",
TargetVersion: "13",
Changer: schemaChangeV12ToV13,
},
{
InitialVersion: "13",
TargetVersion: "14",
Changer: schemaChangeV13ToV14,
},
{
InitialVersion: "14",
TargetVersion: "15",
Changer: schemaChangeV14ToV15,
},
{
InitialVersion: "15",
TargetVersion: "16",
Changer: schemaChangeV15ToV16,
},
{
InitialVersion: "16",
TargetVersion: "17",
Changer: schemaChangeV16ToV17,
},
} }
// build the index for _, account := range accounts {
schemaChanges = make(map[string]SchemaChange) var sus accountSuspensionV18
for _, change := range allChanges { sus.TimeCreated = now
schemaChanges[change.InitialVersion] = change sus.OperName = "*"
sus.Reason = "[unknown]"
susBytes, err := json.Marshal(sus)
if err != nil {
return err
}
tx.Set(suspended+account, string(susBytes), nil)
} }
return nil
}
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
for _, change := range allChanges {
if initialVersion == change.InitialVersion {
return change, true
}
}
return
}
var allChanges = []SchemaChange{
{
InitialVersion: 1,
TargetVersion: 2,
Changer: schemaChangeV1toV2,
},
{
InitialVersion: 2,
TargetVersion: 3,
Changer: schemaChangeV2ToV3,
},
{
InitialVersion: 3,
TargetVersion: 4,
Changer: schemaChangeV3ToV4,
},
{
InitialVersion: 4,
TargetVersion: 5,
Changer: schemaChangeV4ToV5,
},
{
InitialVersion: 5,
TargetVersion: 6,
Changer: schemaChangeV5ToV6,
},
{
InitialVersion: 6,
TargetVersion: 7,
Changer: schemaChangeV6ToV7,
},
{
InitialVersion: 7,
TargetVersion: 8,
Changer: schemaChangeV7ToV8,
},
{
InitialVersion: 8,
TargetVersion: 9,
Changer: schemaChangeV8ToV9,
},
{
InitialVersion: 9,
TargetVersion: 10,
Changer: schemaChangeV9ToV10,
},
{
InitialVersion: 10,
TargetVersion: 11,
Changer: schemaChangeV10ToV11,
},
{
InitialVersion: 11,
TargetVersion: 12,
Changer: schemaChangeV11ToV12,
},
{
InitialVersion: 12,
TargetVersion: 13,
Changer: schemaChangeV12ToV13,
},
{
InitialVersion: 13,
TargetVersion: 14,
Changer: schemaChangeV13ToV14,
},
{
InitialVersion: 14,
TargetVersion: 15,
Changer: schemaChangeV14ToV15,
},
{
InitialVersion: 15,
TargetVersion: 16,
Changer: schemaChangeV15ToV16,
},
{
InitialVersion: 16,
TargetVersion: 17,
Changer: schemaChangeV16ToV17,
},
{
InitialVersion: 17,
TargetVersion: 18,
Changer: schemaChangeV17ToV18,
},
} }

View File

@ -28,6 +28,7 @@ var (
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account") errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks") errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errAccountUnverified = errors.New(`Account is not yet verified`) errAccountUnverified = errors.New(`Account is not yet verified`)
errAccountSuspended = errors.New(`Account has been suspended`)
errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationFailed = errors.New("Account verification failed")
errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
errAccountUpdateFailed = errors.New(`Error while updating your account information`) errAccountUpdateFailed = errors.New(`Error while updating your account information`)

View File

@ -267,7 +267,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
} }
switch err { switch err {
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch: case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch, errAccountSuspended:
return err.Error() return err.Error()
default: default:
// don't expose arbitrary error messages to the user // don't expose arbitrary error messages to the user

View File

@ -15,6 +15,14 @@ import (
"github.com/oragono/oragono/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
const (
// produce a hardcoded version of the database schema
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
// db of the hardcoded version)
importDBSchemaVersion = 18
)
type userImport struct { type userImport struct {
Name string Name string
Hash string Hash string
@ -66,11 +74,7 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion) return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion)
} }
// produce a hardcoded version of the database schema tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil)
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
// db of the hardcoded version)
tx.Set(keySchemaVersion, "17", nil)
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
for username, userInfo := range dbImport.Users { for username, userInfo := range dbImport.Users {

View File

@ -142,7 +142,7 @@ func (mysql *MySQL) fixSchemas() (err error) {
return return
} else if err == nil && schema != latestDbSchema { } else if err == nil && schema != latestDbSchema {
// TODO figure out what to do about schema changes // TODO figure out what to do about schema changes
return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema} return fmt.Errorf("incompatible schema: got %s, expected %s", schema, latestDbSchema)
} else if err != nil { } else if err != nil {
return err return err
} }

View File

@ -6,12 +6,14 @@ package irc
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/sno"
"github.com/oragono/oragono/irc/utils" "github.com/oragono/oragono/irc/utils"
@ -333,19 +335,15 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
}, },
"suspend": { "suspend": {
handler: nsSuspendHandler, handler: nsSuspendHandler,
help: `Syntax: $bSUSPEND <nickname>$b help: `Syntax: $bSUSPEND ADD <nickname> [DURATION duration] [reason]$b
$bSUSPEND DEL <nickname>$b
$bSUSPEND LIST$b
SUSPEND disables an account and disconnects the associated clients.`, Suspending an account disables it (preventing new logins) and disconnects
helpShort: `$bSUSPEND$b disables an account and disconnects the clients`, all associated clients. You can specify a time limit or a reason for
minParams: 1, the suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b
capabs: []string{"accreg"}, command lists all current suspensions.`,
}, helpShort: `$bSUSPEND$b adds or removes an account suspension`,
"unsuspend": {
handler: nsUnsuspendHandler,
help: `Syntax: $bUNSUSPEND <nickname>$b
UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`,
helpShort: `$bUNSUSPEND$b restores access to a suspended account`,
minParams: 1, minParams: 1,
capabs: []string{"accreg"}, capabs: []string{"accreg"},
}, },
@ -810,6 +808,9 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri
for _, channel := range server.accounts.ChannelsForAccount(accountName) { for _, channel := range server.accounts.ChannelsForAccount(accountName) {
nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel)) nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
} }
if account.Suspended != nil {
nsNotice(rb, suspensionToString(client, *account.Suspended))
}
} }
func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
@ -1276,10 +1277,52 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
} }
func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
err := server.accounts.Suspend(params[0]) subCmd := strings.ToLower(params[0])
params = params[1:]
switch subCmd {
case "add":
nsSuspendAddHandler(server, client, command, params, rb)
case "del", "delete", "remove":
nsSuspendRemoveHandler(server, client, command, params, rb)
case "list":
nsSuspendListHandler(server, client, command, params, rb)
default:
nsNotice(rb, client.t("Invalid parameters"))
}
}
func nsSuspendAddHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if len(params) == 0 {
nsNotice(rb, client.t("Invalid parameters"))
return
}
account := params[0]
params = params[1:]
var duration time.Duration
if 2 <= len(params) && strings.ToLower(params[0]) == "duration" {
var err error
cDuration, err := custime.ParseDuration(params[1])
if err != nil {
nsNotice(rb, client.t("Invalid time duration for NS SUSPEND"))
return
}
duration = time.Duration(cDuration)
params = params[2:]
}
var reason string
if len(params) != 0 {
reason = strings.Join(params, " ")
}
name := client.Oper().Name
err := server.accounts.Suspend(account, duration, name, reason)
switch err { switch err {
case nil: case nil:
nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0])) nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), account))
case errAccountDoesNotExist: case errAccountDoesNotExist:
nsNotice(rb, client.t("No such account")) nsNotice(rb, client.t("No such account"))
default: default:
@ -1287,14 +1330,50 @@ func nsSuspendHandler(server *Server, client *Client, command string, params []s
} }
} }
func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsSuspendRemoveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if len(params) == 0 {
nsNotice(rb, client.t("Invalid parameters"))
return
}
err := server.accounts.Unsuspend(params[0]) err := server.accounts.Unsuspend(params[0])
switch err { switch err {
case nil: case nil:
nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0])) nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
case errAccountDoesNotExist: case errAccountDoesNotExist:
nsNotice(rb, client.t("No such account")) nsNotice(rb, client.t("No such account"))
case errNoop:
nsNotice(rb, client.t("Account was not suspended"))
default: default:
nsNotice(rb, client.t("An error occurred")) nsNotice(rb, client.t("An error occurred"))
} }
} }
// sort in reverse order of creation time
type ByCreationTime []AccountSuspension
func (a ByCreationTime) Len() int { return len(a) }
func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
func nsSuspendListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
suspensions := server.accounts.ListSuspended()
sort.Sort(ByCreationTime(suspensions))
nsNotice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions)))
for _, suspension := range suspensions {
nsNotice(rb, suspensionToString(client, suspension))
}
}
func suspensionToString(client *Client, suspension AccountSuspension) (result string) {
duration := client.t("indefinite")
if suspension.Duration != time.Duration(0) {
duration = suspension.Duration.String()
}
ts := suspension.TimeCreated.Format(time.RFC1123)
reason := client.t("No reason given.")
if suspension.Reason != "" {
reason = fmt.Sprintf(client.t("Reason: %s"), suspension.Reason)
}
return fmt.Sprintf(client.t("Account %s suspended at %s. Duration: %s. %s"), suspension.AccountName, ts, duration, reason)
}

View File

@ -71,12 +71,12 @@ func SafeErrorParam(param string) string {
} }
type IncompatibleSchemaError struct { type IncompatibleSchemaError struct {
CurrentVersion string CurrentVersion int
RequiredVersion string RequiredVersion int
} }
func (err *IncompatibleSchemaError) Error() string { func (err *IncompatibleSchemaError) Error() string {
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion) return fmt.Sprintf("Database requires update. Expected schema v%d, got v%d", err.RequiredVersion, err.CurrentVersion)
} }
func NanoToTimestamp(nanotime int64) string { func NanoToTimestamp(nanotime int64) string {