Browse Source

fix #1274

Enhancements to NS SUSPEND, including stored metadata and the ability
to list suspensions
tags/v2.4.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
1f6afa31d6
5 changed files with 195 additions and 30 deletions
  1. 97
    12
      irc/accounts.go
  2. 1
    1
      irc/database.go
  3. 1
    0
      irc/errors.go
  4. 1
    1
      irc/handlers.go
  5. 95
    16
      irc/nickserv.go

+ 97
- 12
irc/accounts.go View File

@@ -40,8 +40,9 @@ const (
40 40
 	keyAccountChannels         = "account.channels %s" // channels registered to the account
41 41
 	keyAccountJoinedChannels   = "account.joinedto %s" // channels a persistent client has joined
42 42
 	keyAccountLastSeen         = "account.lastseen %s"
43
-	keyAccountModes            = "account.modes %s"    // user modes for the always-on client as a string
44
-	keyAccountRealname         = "account.realname %s" // client realname stored as string
43
+	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
44
+	keyAccountRealname         = "account.realname %s"  // client realname stored as string
45
+	keyAccountSuspended        = "account.suspended %s" // client realname stored as string
45 46
 
46 47
 	maxCertfpsPerAccount = 5
47 48
 )
@@ -117,7 +118,7 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) {
117 118
 
118 119
 	for _, accountName := range accounts {
119 120
 		account, err := am.LoadAccount(accountName)
120
-		if err == nil && account.Verified &&
121
+		if err == nil && (account.Verified && account.Suspended == nil) &&
121 122
 			persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
122 123
 			am.server.AddAlwaysOnClient(
123 124
 				account,
@@ -1035,6 +1036,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
1035 1036
 	if !account.Verified {
1036 1037
 		err = errAccountUnverified
1037 1038
 		return
1039
+	} else if account.Suspended != nil {
1040
+		err = errAccountSuspended
1041
+		return
1038 1042
 	}
1039 1043
 
1040 1044
 	switch account.Credentials.Version {
@@ -1230,6 +1234,15 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName str
1230 1234
 			am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
1231 1235
 		}
1232 1236
 	}
1237
+	if raw.Suspended != "" {
1238
+		sus := new(AccountSuspension)
1239
+		e := json.Unmarshal([]byte(raw.Suspended), sus)
1240
+		if e != nil {
1241
+			am.server.logger.Error("internal", "corrupt suspension data", result.Name, e.Error())
1242
+		} else {
1243
+			result.Suspended = sus
1244
+		}
1245
+	}
1233 1246
 	return
1234 1247
 }
1235 1248
 
@@ -1243,6 +1256,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
1243 1256
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1244 1257
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1245 1258
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1259
+	suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
1246 1260
 
1247 1261
 	_, e := tx.Get(accountKey)
1248 1262
 	if e == buntdb.ErrNotFound {
@@ -1257,6 +1271,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
1257 1271
 	result.AdditionalNicks, _ = tx.Get(nicksKey)
1258 1272
 	result.VHost, _ = tx.Get(vhostKey)
1259 1273
 	result.Settings, _ = tx.Get(settingsKey)
1274
+	result.Suspended, _ = tx.Get(suspendedKey)
1260 1275
 
1261 1276
 	if _, e = tx.Get(verifiedKey); e == nil {
1262 1277
 		result.Verified = true
@@ -1265,20 +1280,44 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
1265 1280
 	return
1266 1281
 }
1267 1282
 
1268
-func (am *AccountManager) Suspend(accountName string) (err error) {
1283
+type AccountSuspension struct {
1284
+	AccountName string `json:"AccountName,omitempty"`
1285
+	TimeCreated time.Time
1286
+	Duration    time.Duration
1287
+	OperName    string
1288
+	Reason      string
1289
+}
1290
+
1291
+func (am *AccountManager) Suspend(accountName string, duration time.Duration, operName, reason string) (err error) {
1269 1292
 	account, err := CasefoldName(accountName)
1270 1293
 	if err != nil {
1271 1294
 		return errAccountDoesNotExist
1272 1295
 	}
1273 1296
 
1297
+	suspension := AccountSuspension{
1298
+		TimeCreated: time.Now().UTC(),
1299
+		Duration:    duration,
1300
+		OperName:    operName,
1301
+		Reason:      reason,
1302
+	}
1303
+	suspensionStr, err := json.Marshal(suspension)
1304
+	if err != nil {
1305
+		am.server.logger.Error("internal", "suspension json unserializable", err.Error())
1306
+		return errAccountDoesNotExist
1307
+	}
1308
+
1274 1309
 	existsKey := fmt.Sprintf(keyAccountExists, account)
1275
-	verifiedKey := fmt.Sprintf(keyAccountVerified, account)
1310
+	suspensionKey := fmt.Sprintf(keyAccountSuspended, account)
1311
+	var setOptions *buntdb.SetOptions
1312
+	if duration != time.Duration(0) {
1313
+		setOptions = &buntdb.SetOptions{Expires: true, TTL: duration}
1314
+	}
1276 1315
 	err = am.server.store.Update(func(tx *buntdb.Tx) error {
1277 1316
 		_, err := tx.Get(existsKey)
1278 1317
 		if err != nil {
1279 1318
 			return errAccountDoesNotExist
1280 1319
 		}
1281
-		_, err = tx.Delete(verifiedKey)
1320
+		_, _, err = tx.Set(suspensionKey, string(suspensionStr), setOptions)
1282 1321
 		return err
1283 1322
 	})
1284 1323
 
@@ -1293,7 +1332,13 @@ func (am *AccountManager) Suspend(accountName string) (err error) {
1293 1332
 	delete(am.accountToClients, account)
1294 1333
 	am.Unlock()
1295 1334
 
1296
-	am.killClients(clients)
1335
+	// kill clients, sending them the reason
1336
+	suspension.AccountName = accountName
1337
+	for _, client := range clients {
1338
+		client.Logout()
1339
+		client.Quit(suspensionToString(client, suspension), nil)
1340
+		client.destroy(nil)
1341
+	}
1297 1342
 	return nil
1298 1343
 }
1299 1344
 
@@ -1312,20 +1357,53 @@ func (am *AccountManager) Unsuspend(account string) (err error) {
1312 1357
 	}
1313 1358
 
1314 1359
 	existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
1315
-	verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount)
1360
+	suspensionKey := fmt.Sprintf(keyAccountSuspended, account)
1316 1361
 	err = am.server.store.Update(func(tx *buntdb.Tx) error {
1317 1362
 		_, err := tx.Get(existsKey)
1318 1363
 		if err != nil {
1319 1364
 			return errAccountDoesNotExist
1320 1365
 		}
1321
-		tx.Set(verifiedKey, "1", nil)
1366
+		_, err = tx.Delete(suspensionKey)
1367
+		if err != nil {
1368
+			return errNoop
1369
+		}
1322 1370
 		return nil
1323 1371
 	})
1324 1372
 
1325
-	if err != nil {
1326
-		return errAccountDoesNotExist
1373
+	return err
1374
+}
1375
+
1376
+func (am *AccountManager) ListSuspended() (result []AccountSuspension) {
1377
+	var names []string
1378
+	var raw []string
1379
+
1380
+	prefix := fmt.Sprintf(keyAccountSuspended, "")
1381
+	am.server.store.View(func(tx *buntdb.Tx) error {
1382
+		err := tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
1383
+			if !strings.HasPrefix(key, prefix) {
1384
+				return false
1385
+			}
1386
+			raw = append(raw, value)
1387
+			cfname := strings.TrimPrefix(key, prefix)
1388
+			name, _ := tx.Get(fmt.Sprintf(keyAccountName, cfname))
1389
+			names = append(names, name)
1390
+			return true
1391
+		})
1392
+		return err
1393
+	})
1394
+
1395
+	result = make([]AccountSuspension, 0, len(raw))
1396
+	for i := 0; i < len(raw); i++ {
1397
+		var sus AccountSuspension
1398
+		err := json.Unmarshal([]byte(raw[i]), &sus)
1399
+		if err != nil {
1400
+			am.server.logger.Error("internal", "corrupt data for suspension", names[i], err.Error())
1401
+			continue
1402
+		}
1403
+		sus.AccountName = names[i]
1404
+		result = append(result, sus)
1327 1405
 	}
1328
-	return nil
1406
+	return
1329 1407
 }
1330 1408
 
1331 1409
 func (am *AccountManager) Unregister(account string, erase bool) error {
@@ -1351,6 +1429,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1351 1429
 	unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
1352 1430
 	modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
1353 1431
 	realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
1432
+	suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
1354 1433
 
1355 1434
 	var clients []*Client
1356 1435
 	defer func() {
@@ -1410,6 +1489,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1410 1489
 		tx.Delete(lastSeenKey)
1411 1490
 		tx.Delete(modesKey)
1412 1491
 		tx.Delete(realnameKey)
1492
+		tx.Delete(suspendedKey)
1413 1493
 
1414 1494
 		return nil
1415 1495
 	})
@@ -1491,6 +1571,9 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
1491 1571
 		} else if !clientAccount.Verified {
1492 1572
 			err = errAccountUnverified
1493 1573
 			return
1574
+		} else if clientAccount.Suspended != nil {
1575
+			err = errAccountSuspended
1576
+			return
1494 1577
 		}
1495 1578
 		// TODO(#1109) clean this check up?
1496 1579
 		if client.registered {
@@ -1882,6 +1965,7 @@ type ClientAccount struct {
1882 1965
 	RegisteredAt    time.Time
1883 1966
 	Credentials     AccountCredentials
1884 1967
 	Verified        bool
1968
+	Suspended       *AccountSuspension
1885 1969
 	AdditionalNicks []string
1886 1970
 	VHost           VHostInfo
1887 1971
 	Settings        AccountSettings
@@ -1897,4 +1981,5 @@ type rawClientAccount struct {
1897 1981
 	AdditionalNicks string
1898 1982
 	VHost           string
1899 1983
 	Settings        string
1984
+	Suspended       string
1900 1985
 }

+ 1
- 1
irc/database.go View File

@@ -857,7 +857,7 @@ func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
857 857
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
858 858
 	for _, change := range allChanges {
859 859
 		if initialVersion == change.InitialVersion {
860
-			return result, true
860
+			return change, true
861 861
 		}
862 862
 	}
863 863
 	return

+ 1
- 0
irc/errors.go View File

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

+ 1
- 1
irc/handlers.go View File

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

+ 95
- 16
irc/nickserv.go View File

@@ -6,12 +6,14 @@ package irc
6 6
 import (
7 7
 	"fmt"
8 8
 	"regexp"
9
+	"sort"
9 10
 	"strconv"
10 11
 	"strings"
11 12
 	"time"
12 13
 
13 14
 	"github.com/goshuirc/irc-go/ircfmt"
14 15
 
16
+	"github.com/oragono/oragono/irc/custime"
15 17
 	"github.com/oragono/oragono/irc/passwd"
16 18
 	"github.com/oragono/oragono/irc/sno"
17 19
 	"github.com/oragono/oragono/irc/utils"
@@ -333,19 +335,15 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
333 335
 		},
334 336
 		"suspend": {
335 337
 			handler: nsSuspendHandler,
336
-			help: `Syntax: $bSUSPEND <nickname>$b
337
-
338
-SUSPEND disables an account and disconnects the associated clients.`,
339
-			helpShort: `$bSUSPEND$b disables an account and disconnects the clients`,
340
-			minParams: 1,
341
-			capabs:    []string{"accreg"},
342
-		},
343
-		"unsuspend": {
344
-			handler: nsUnsuspendHandler,
345
-			help: `Syntax: $bUNSUSPEND <nickname>$b
346
-
347
-UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`,
348
-			helpShort: `$bUNSUSPEND$b restores access to a suspended account`,
338
+			help: `Syntax: $bSUSPEND ADD <nickname> [DURATION duration] [reason]$b
339
+        $bSUSPEND DEL <nickname>$b
340
+        $bSUSPEND LIST$b
341
+
342
+Suspending an account disables it (preventing new logins) and disconnects
343
+all associated clients. You can specify a time limit or a reason for
344
+the suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b
345
+command lists all current suspensions.`,
346
+			helpShort: `$bSUSPEND$b adds or removes an account suspension`,
349 347
 			minParams: 1,
350 348
 			capabs:    []string{"accreg"},
351 349
 		},
@@ -810,6 +808,9 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri
810 808
 	for _, channel := range server.accounts.ChannelsForAccount(accountName) {
811 809
 		nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
812 810
 	}
811
+	if account.Suspended != nil {
812
+		nsNotice(rb, suspensionToString(client, *account.Suspended))
813
+	}
813 814
 }
814 815
 
815 816
 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
1276 1277
 }
1277 1278
 
1278 1279
 func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1279
-	err := server.accounts.Suspend(params[0])
1280
+	subCmd := strings.ToLower(params[0])
1281
+	params = params[1:]
1282
+	switch subCmd {
1283
+	case "add":
1284
+		nsSuspendAddHandler(server, client, command, params, rb)
1285
+	case "del", "delete", "remove":
1286
+		nsSuspendRemoveHandler(server, client, command, params, rb)
1287
+	case "list":
1288
+		nsSuspendListHandler(server, client, command, params, rb)
1289
+	default:
1290
+		nsNotice(rb, client.t("Invalid parameters"))
1291
+	}
1292
+}
1293
+
1294
+func nsSuspendAddHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1295
+	if len(params) == 0 {
1296
+		nsNotice(rb, client.t("Invalid parameters"))
1297
+		return
1298
+	}
1299
+
1300
+	account := params[0]
1301
+	params = params[1:]
1302
+
1303
+	var duration time.Duration
1304
+	if 2 <= len(params) && strings.ToLower(params[0]) == "duration" {
1305
+		var err error
1306
+		cDuration, err := custime.ParseDuration(params[1])
1307
+		if err != nil {
1308
+			nsNotice(rb, client.t("Invalid time duration for NS SUSPEND"))
1309
+			return
1310
+		}
1311
+		duration = time.Duration(cDuration)
1312
+		params = params[2:]
1313
+	}
1314
+
1315
+	var reason string
1316
+	if len(params) != 0 {
1317
+		reason = strings.Join(params, " ")
1318
+	}
1319
+
1320
+	name := client.Oper().Name
1321
+
1322
+	err := server.accounts.Suspend(account, duration, name, reason)
1280 1323
 	switch err {
1281 1324
 	case nil:
1282
-		nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0]))
1325
+		nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), account))
1283 1326
 	case errAccountDoesNotExist:
1284 1327
 		nsNotice(rb, client.t("No such account"))
1285 1328
 	default:
@@ -1287,14 +1330,50 @@ func nsSuspendHandler(server *Server, client *Client, command string, params []s
1287 1330
 	}
1288 1331
 }
1289 1332
 
1290
-func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1333
+func nsSuspendRemoveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1334
+	if len(params) == 0 {
1335
+		nsNotice(rb, client.t("Invalid parameters"))
1336
+		return
1337
+	}
1338
+
1291 1339
 	err := server.accounts.Unsuspend(params[0])
1292 1340
 	switch err {
1293 1341
 	case nil:
1294 1342
 		nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
1295 1343
 	case errAccountDoesNotExist:
1296 1344
 		nsNotice(rb, client.t("No such account"))
1345
+	case errNoop:
1346
+		nsNotice(rb, client.t("Account was not suspended"))
1297 1347
 	default:
1298 1348
 		nsNotice(rb, client.t("An error occurred"))
1299 1349
 	}
1300 1350
 }
1351
+
1352
+// sort in reverse order of creation time
1353
+type ByCreationTime []AccountSuspension
1354
+
1355
+func (a ByCreationTime) Len() int           { return len(a) }
1356
+func (a ByCreationTime) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
1357
+func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
1358
+
1359
+func nsSuspendListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1360
+	suspensions := server.accounts.ListSuspended()
1361
+	sort.Sort(ByCreationTime(suspensions))
1362
+	nsNotice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions)))
1363
+	for _, suspension := range suspensions {
1364
+		nsNotice(rb, suspensionToString(client, suspension))
1365
+	}
1366
+}
1367
+
1368
+func suspensionToString(client *Client, suspension AccountSuspension) (result string) {
1369
+	duration := client.t("indefinite")
1370
+	if suspension.Duration != time.Duration(0) {
1371
+		duration = suspension.Duration.String()
1372
+	}
1373
+	ts := suspension.TimeCreated.Format(time.RFC1123)
1374
+	reason := client.t("No reason given.")
1375
+	if suspension.Reason != "" {
1376
+		reason = fmt.Sprintf(client.t("Reason: %s"), suspension.Reason)
1377
+	}
1378
+	return fmt.Sprintf(client.t("Account %s suspended at %s. Duration: %s. %s"), suspension.AccountName, ts, duration, reason)
1379
+}

Loading…
Cancel
Save