per-user settings for nickname enforcement

This commit is contained in:
Shivaram Lingamneni 2019-01-02 10:08:44 -05:00
parent d0ded906d4
commit 2ee89b15b3
7 changed files with 216 additions and 39 deletions

View File

@ -30,6 +30,7 @@ const (
keyAccountRegTime = "account.registered.time %s" keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s" keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s" keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountEnforcement = "account.customenforcement %s"
keyAccountVHost = "account.vhost %s" keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s" keyCertToAccount = "account.creds.certfp %s"
@ -53,12 +54,14 @@ type AccountManager struct {
// track clients logged in to accounts // track clients logged in to accounts
accountToClients map[string][]*Client accountToClients map[string][]*Client
nickToAccount map[string]string nickToAccount map[string]string
accountToMethod map[string]NickReservationMethod
} }
func NewAccountManager(server *Server) *AccountManager { func NewAccountManager(server *Server) *AccountManager {
am := AccountManager{ am := AccountManager{
accountToClients: make(map[string][]*Client), accountToClients: make(map[string][]*Client),
nickToAccount: make(map[string]string), nickToAccount: make(map[string]string),
accountToMethod: make(map[string]NickReservationMethod),
server: server, server: server,
} }
@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
return return
} }
result := make(map[string]string) nickToAccount := make(map[string]string)
accountToMethod := make(map[string]NickReservationMethod)
existsPrefix := fmt.Sprintf(keyAccountExists, "") existsPrefix := fmt.Sprintf(keyAccountExists, "")
am.serialCacheUpdateMutex.Lock() am.serialCacheUpdateMutex.Lock()
@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() {
if !strings.HasPrefix(key, existsPrefix) { if !strings.HasPrefix(key, existsPrefix) {
return false return false
} }
accountName := strings.TrimPrefix(key, existsPrefix)
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil { account := strings.TrimPrefix(key, existsPrefix)
result[accountName] = accountName if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
nickToAccount[account] = account
} }
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil { if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
additionalNicks := unmarshalReservedNicks(rawNicks) additionalNicks := unmarshalReservedNicks(rawNicks)
for _, nick := range additionalNicks { for _, nick := range additionalNicks {
result[nick] = accountName nickToAccount[nick] = account
}
}
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
method, err := nickReservationFromString(methodStr)
if err == nil {
accountToMethod[account] = method
} }
} }
return true return true
@ -102,7 +114,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error()) am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
} else { } else {
am.Lock() am.Lock()
am.nickToAccount = result am.nickToAccount = nickToAccount
am.accountToMethod = accountToMethod
am.Unlock() am.Unlock()
} }
} }
@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string {
return am.nickToAccount[cfnick] return am.nickToAccount[cfnick]
} }
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership.
func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
cfnick, err := CasefoldName(nick)
if err != nil {
return
}
config := am.server.Config()
if !config.Accounts.NickReservation.Enabled {
method = NickReservationNone
return
}
am.RLock()
defer am.RUnlock()
account = am.nickToAccount[cfnick]
method = am.accountToMethod[account]
// if they don't have a custom setting, or customization is disabled, use the default
if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
method = config.Accounts.NickReservation.Method
}
if method == NickReservationOptional {
// enforcement was explicitly enabled neither in the config or by the user
method = NickReservationNone
}
return
}
// Looks up the enforcement method stored in the database for an account
// (typically you want EnforcementStatus instead, which respects the config)
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
am.RLock()
defer am.RUnlock()
return nickReservationToString(am.accountToMethod[account])
}
// Sets a custom enforcement method for an account and stores it in the database.
func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
config := am.server.Config()
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
return errFeatureDisabled
}
var serialized string
if method == NickReservationOptional {
serialized = "" // normally this is "default", but we're going to delete the key
} else {
serialized = nickReservationToString(method)
}
key := fmt.Sprintf(keyAccountEnforcement, account)
am.Lock()
defer am.Unlock()
currentMethod := am.accountToMethod[account]
if method != currentMethod {
if method == NickReservationOptional {
delete(am.accountToMethod, account)
} else {
am.accountToMethod[account] = method
}
return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
if serialized != "" {
_, _, err = tx.Set(key, nickReservationToString(method), nil)
} else {
_, err = tx.Delete(key)
}
return
})
}
return nil
}
func (am *AccountManager) AccountToClients(account string) (result []*Client) { func (am *AccountManager) AccountToClients(account string) (result []*Client) {
cfaccount, err := CasefoldName(account) cfaccount, err := CasefoldName(account)
if err != nil { if err != nil {
@ -992,10 +1083,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
func (am *AccountManager) Login(client *Client, account ClientAccount) { func (am *AccountManager) Login(client *Client, account ClientAccount) {
changed := client.SetAccountName(account.Name) changed := client.SetAccountName(account.Name)
if changed { if !changed {
go client.nickTimer.Touch() return
} }
client.nickTimer.Touch()
am.applyVHostInfo(client, account.VHost) am.applyVHostInfo(client, account.VHost)
casefoldedAccount := client.Account() casefoldedAccount := client.Account()

View File

@ -119,12 +119,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
return err return err
} }
var reservedAccount string reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
var method NickReservationMethod
if client.server.AccountConfig().NickReservation.Enabled {
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
method = client.server.AccountConfig().NickReservation.Method
}
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()

View File

@ -8,7 +8,6 @@ package irc
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -105,10 +104,50 @@ type VHostConfig struct {
type NickReservationMethod int type NickReservationMethod int
const ( const (
NickReservationWithTimeout NickReservationMethod = iota // NickReservationOptional is the zero value; it serializes to
// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
// in both cases, it means "defer to the other source of truth", i.e.,
// in the config, defer to the user's custom setting, and as a custom setting,
// defer to the default in the config. if both are NickReservationOptional then
// there is no enforcement.
NickReservationOptional NickReservationMethod = iota
NickReservationNone
NickReservationWithTimeout
NickReservationStrict NickReservationStrict
) )
func nickReservationToString(method NickReservationMethod) string {
switch method {
case NickReservationOptional:
return "default"
case NickReservationNone:
return "none"
case NickReservationWithTimeout:
return "timeout"
case NickReservationStrict:
return "strict"
default:
return ""
}
}
func nickReservationFromString(method string) (NickReservationMethod, error) {
switch method {
case "default":
return NickReservationOptional, nil
case "optional":
return NickReservationOptional, nil
case "none":
return NickReservationNone, nil
case "timeout":
return NickReservationWithTimeout, nil
case "strict":
return NickReservationStrict, nil
default:
return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
}
}
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error { func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig, raw string var orig, raw string
var err error var err error
@ -118,22 +157,20 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
if raw, err = Casefold(orig); err != nil { if raw, err = Casefold(orig); err != nil {
return err return err
} }
if raw == "timeout" { method, err := nickReservationFromString(raw)
*nr = NickReservationWithTimeout if err == nil {
} else if raw == "strict" { *nr = method
*nr = NickReservationStrict
} else {
return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig))
} }
return nil return err
} }
type NickReservationConfig struct { type NickReservationConfig struct {
Enabled bool Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"` AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickReservationMethod Method NickReservationMethod
RenameTimeout time.Duration `yaml:"rename-timeout"` AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenamePrefix string `yaml:"rename-prefix"` RenameTimeout time.Duration `yaml:"rename-timeout"`
RenamePrefix string `yaml:"rename-prefix"`
} }
// ChannelRegistrationConfig controls channel registration. // ChannelRegistrationConfig controls channel registration.

View File

@ -40,6 +40,7 @@ var (
errSaslFail = errors.New("SASL failed") errSaslFail = errors.New("SASL failed")
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token") errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
errInvalidUsername = errors.New("Invalid username") errInvalidUsername = errors.New("Invalid username")
errFeatureDisabled = errors.New("That feature is disabled")
) )
// Socket Errors // Socket Errors

View File

@ -189,14 +189,14 @@ type NickTimer struct {
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled) // NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
func NewNickTimer(client *Client) *NickTimer { func NewNickTimer(client *Client) *NickTimer {
config := client.server.AccountConfig().NickReservation config := client.server.AccountConfig().NickReservation
if !(config.Enabled && config.Method == NickReservationWithTimeout) { if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) {
return nil return nil
} }
nt := NickTimer{
return &NickTimer{
client: client, client: client,
timeout: config.RenameTimeout, timeout: config.RenameTimeout,
} }
return &nt
} }
// Touch records a nick change and updates the timer as necessary // Touch records a nick change and updates the timer as necessary
@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() {
nick := nt.client.NickCasefolded() nick := nt.client.NickCasefolded()
account := nt.client.Account() account := nt.client.Account()
accountForNick := nt.client.server.accounts.NickToAccount(nick) accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
enforceTimeout := method == NickReservationWithTimeout
var shouldWarn bool var shouldWarn bool
@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() {
nt.accountForNick = accountForNick nt.accountForNick = accountForNick
delinquent := accountForNick != "" && accountForNick != account delinquent := accountForNick != "" && accountForNick != account
if nt.timer != nil && (!delinquent || accountChanged) { if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) {
nt.timer.Stop() nt.timer.Stop()
nt.timer = nil nt.timer = nil
} }
if delinquent && accountChanged { if enforceTimeout && delinquent && accountChanged {
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
shouldWarn = true shouldWarn = true
} }

View File

@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool {
return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
} }
func nsEnforceEnabled(server *Server) bool {
config := server.Config()
return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement
}
const nickservHelp = `NickServ lets you register and login to an account. const nickservHelp = `NickServ lets you register and login to an account.
To see in-depth help for a specific NickServ command, try: To see in-depth help for a specific NickServ command, try:
@ -44,6 +49,22 @@ DROP de-links the given (or your current) nickname from your user account.`,
enabled: servCmdRequiresAccreg, enabled: servCmdRequiresAccreg,
authRequired: true, authRequired: true,
}, },
"enforce": {
handler: nsEnforceHandler,
help: `Syntax: $bENFORCE [method]$b
ENFORCE lets you specify a custom enforcement mechanism for your registered
nicknames. Your options are:
1. 'none' [no enforcement, overriding the server default]
2. 'timeout' [anyone using the nick must authenticate before a deadline,
or else they will be renamed]
3. 'strict' [you must already be authenticated to use the nick]
4. 'default' [use the server default]
With no arguments, queries your current enforcement status.`,
helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`,
authRequired: true,
enabled: nsEnforceEnabled,
},
"ghost": { "ghost": {
handler: nsGhostHandler, handler: nsGhostHandler,
help: `Syntax: $bGHOST <nickname>$b help: `Syntax: $bGHOST <nickname>$b
@ -464,3 +485,25 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
nsNotice(rb, client.t("Password could not be changed due to server error")) nsNotice(rb, client.t("Password could not be changed due to server error"))
} }
} }
func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
arg := strings.TrimSpace(params)
if arg == "" {
status := server.accounts.getStoredEnforcementStatus(client.Account())
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
} else {
method, err := nickReservationFromString(arg)
if err != nil {
nsNotice(rb, client.t("Invalid parameters"))
return
}
err = server.accounts.SetEnforcementStatus(client.Account(), method)
if err == nil {
nsNotice(rb, client.t("Enforcement method set"))
} else {
server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
nsNotice(rb, client.t("An error occurred"))
}
}
}

View File

@ -206,12 +206,19 @@ accounts:
additional-nick-limit: 2 additional-nick-limit: 2
# method describes how nickname reservation is handled # method describes how nickname reservation is handled
# timeout: let the user change to the registered nickname, give them X seconds # already logged-in using SASL or NickServ
# to login and then rename them if they haven't done so # timeout: let the user change to the registered nickname, give them X seconds
# strict: don't let the user change to the registered nickname unless they're # to login and then rename them if they haven't done so
# already logged-in using SASL or NickServ # strict: don't let the user change to the registered nickname unless they're
# already logged-in using SASL or NickServ
# optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice
method: timeout method: timeout
# allow users to set their own nickname enforcement status, e.g.,
# to opt in to strict enforcement
allow-custom-enforcement: true
# rename-timeout - this is how long users have 'til they're renamed # rename-timeout - this is how long users have 'til they're renamed
rename-timeout: 30s rename-timeout: 30s