Add oper classes, for more finely-grained control

This commit is contained in:
Daniel Oaks 2016-10-23 10:47:11 +10:00
parent a5395b5fe2
commit 64bdedaee2
5 changed files with 212 additions and 15 deletions

View File

@ -10,8 +10,10 @@ New release of Oragono!
### Security ### Security
### Added ### Added
* Operator classes, allowing for more finely-grained permissions for operators.
### Changed ### Changed
* In the config file, "operator" changed to "opers", and new oper class is required.
### Removed ### Removed

View File

@ -37,6 +37,7 @@ type Client struct {
capVersion CapVersion capVersion CapVersion
certfp string certfp string
channels ChannelSet channels ChannelSet
class *OperClass
ctime time.Time ctime time.Time
flags map[UserMode]bool flags map[UserMode]bool
isDestroyed bool isDestroyed bool
@ -50,6 +51,7 @@ type Client struct {
nickCasefolded string nickCasefolded string
nickMaskString string // cache for nickmask string since it's used with lots of replies nickMaskString string // cache for nickmask string since it's used with lots of replies
nickMaskCasefolded string nickMaskCasefolded string
operName string
quitTimer *time.Timer quitTimer *time.Timer
realname string realname string
registered bool registered bool
@ -347,6 +349,12 @@ func (client *Client) destroy() {
friends := client.Friends() friends := client.Friends()
friends.Remove(client) friends.Remove(client)
// remove from opers list
_, exists := client.server.currentOpers[client]
if exists {
delete(client.server.currentOpers, client)
}
// alert monitors // alert monitors
for _, mClient := range client.server.monitoring[client.nickCasefolded] { for _, mClient := range client.server.monitoring[client.nickCasefolded] {
mClient.Send(nil, client.server.name, RPL_MONOFFLINE, mClient.nick, client.nick) mClient.Send(nil, client.server.name, RPL_MONOFFLINE, mClient.nick, client.nick)

View File

@ -8,6 +8,7 @@ package irc
import ( import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -65,6 +66,26 @@ type AccountRegistrationConfig struct {
} }
} }
type OperClassConfig struct {
Title string
Extends string
Capabilities []string
}
type OperConfig struct {
Class string
Vhost string
Password string
}
func (conf *OperConfig) PasswordBytes() []byte {
bytes, err := DecodePasswordHash(conf.Password)
if err != nil {
log.Fatal("decode password error: ", err)
}
return bytes
}
type Config struct { type Config struct {
Network struct { Network struct {
Name string Name string
@ -92,7 +113,9 @@ type Config struct {
Accounts AccountRegistrationConfig Accounts AccountRegistrationConfig
} }
Operator map[string]*PassConfig OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
Opers map[string]*OperConfig
Limits struct { Limits struct {
NickLen uint `yaml:"nicklen"` NickLen uint `yaml:"nicklen"`
@ -105,17 +128,97 @@ type Config struct {
} }
} }
func (conf *Config) Operators() map[string][]byte { type OperClass struct {
operators := make(map[string][]byte) Title string
for name, opConf := range conf.Operator { Capabilities map[string]bool // map to make lookups much easier
name, err := CasefoldName(name) }
if err == nil {
operators[name] = opConf.PasswordBytes() func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
} else { ocs := make(map[string]OperClass)
log.Println("Could not casefold oper name:", err.Error())
// loop from no extends to most extended, breaking if we can't add any more
lenOfLastOcs := -1
for {
if lenOfLastOcs == len(ocs) {
return nil, errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
}
lenOfLastOcs = len(ocs)
var anyMissing bool
for name, info := range conf.OperClasses {
_, exists := ocs[name]
_, extendsExists := ocs[info.Extends]
if exists {
// class already exists
continue
} else if len(info.Extends) > 0 && !extendsExists {
// class we extend on doesn't exist
_, exists := conf.OperClasses[info.Extends]
if !exists {
return nil, fmt.Errorf("Operclass [%s] extends [%s], which doesn't exist", name, info.Extends)
}
anyMissing = true
continue
}
// create new operclass
var oc OperClass
oc.Capabilities = make(map[string]bool)
// get inhereted info from other operclasses
if len(info.Extends) > 0 {
einfo, _ := ocs[info.Extends]
for capab := range einfo.Capabilities {
oc.Capabilities[capab] = true
}
}
// add our own info
oc.Title = info.Title
for _, capab := range info.Capabilities {
oc.Capabilities[capab] = true
}
ocs[name] = oc
}
if !anyMissing {
// we've got every operclass!
break
} }
} }
return operators
return &ocs, nil
}
type Oper struct {
Class *OperClass
Pass []byte
}
func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error) {
operators := make(map[string]Oper)
for name, opConf := range conf.Opers {
var oper Oper
// oper name
name, err := CasefoldName(name)
if err != nil {
return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error())
}
oper.Pass = opConf.PasswordBytes()
class, exists := (*oc)[opConf.Class]
if !exists {
return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class)
}
oper.Class = &class
// successful, attach to list of opers
operators[name] = oper
}
return operators, nil
} }
func (conf *Config) TLSListeners() map[string]*tls.Config { func (conf *Config) TLSListeners() map[string]*tls.Config {
@ -169,5 +272,6 @@ func LoadConfig(filename string) (config *Config, err error) {
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 { if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
return nil, errors.New("Limits aren't setup properly, check them and make them sane") return nil, errors.New("Limits aren't setup properly, check them and make them sane")
} }
return config, nil return config, nil
} }

View File

@ -66,6 +66,7 @@ type Server struct {
commands chan Command commands chan Command
configFilename string configFilename string
ctime time.Time ctime time.Time
currentOpers map[*Client]bool
store buntdb.DB store buntdb.DB
idle chan *Client idle chan *Client
limits Limits limits Limits
@ -78,7 +79,8 @@ type Server struct {
nameCasefolded string nameCasefolded string
networkName string networkName string
newConns chan clientConn newConns chan clientConn
operators map[string][]byte operators map[string]Oper
operclasses map[string]OperClass
password []byte password []byte
passwords *PasswordManager passwords *PasswordManager
rehashMutex sync.Mutex rehashMutex sync.Mutex
@ -115,6 +117,15 @@ func NewServer(configFilename string, config *Config) *Server {
SupportedCapabilities[SASL] = true SupportedCapabilities[SASL] = true
} }
operClasses, err := config.OperatorClasses()
if err != nil {
log.Fatal("Error loading oper classes:", err.Error())
}
opers, err := config.Operators(operClasses)
if err != nil {
log.Fatal("Error loading operators:", err.Error())
}
server := &Server{ server := &Server{
accounts: make(map[string]*ClientAccount), accounts: make(map[string]*ClientAccount),
authenticationEnabled: config.AuthenticationEnabled, authenticationEnabled: config.AuthenticationEnabled,
@ -123,6 +134,7 @@ func NewServer(configFilename string, config *Config) *Server {
commands: make(chan Command), commands: make(chan Command),
configFilename: configFilename, configFilename: configFilename,
ctime: time.Now(), ctime: time.Now(),
currentOpers: make(map[*Client]bool),
idle: make(chan *Client), idle: make(chan *Client),
limits: Limits{ limits: Limits{
AwayLen: int(config.Limits.AwayLen), AwayLen: int(config.Limits.AwayLen),
@ -138,7 +150,8 @@ func NewServer(configFilename string, config *Config) *Server {
nameCasefolded: casefoldedName, nameCasefolded: casefoldedName,
networkName: config.Network.Name, networkName: config.Network.Name,
newConns: make(chan clientConn), newConns: make(chan clientConn),
operators: config.Operators(), operclasses: *operClasses,
operators: opers,
signals: make(chan os.Signal, len(SERVER_SIGNALS)), signals: make(chan os.Signal, len(SERVER_SIGNALS)),
rehashSignal: make(chan os.Signal, 1), rehashSignal: make(chan os.Signal, 1),
whoWas: NewWhoWasList(config.Limits.WhowasEntries), whoWas: NewWhoWasList(config.Limits.WhowasEntries),
@ -874,17 +887,24 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, "Password incorrect") client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, "Password incorrect")
return true return true
} }
hash := server.operators[name] hash := server.operators[name].Pass
password := []byte(msg.Params[1]) password := []byte(msg.Params[1])
err = ComparePassword(hash, password) err = ComparePassword(hash, password)
if (hash == nil) || (err != nil) { if (hash == nil) || (err != nil) {
fmt.Println("2", hash)
client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, "Password incorrect") client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, "Password incorrect")
return true return true
} }
client.flags[Operator] = true client.flags[Operator] = true
client.operName = name
client.class = server.operators[name].Class
server.currentOpers[client] = true
//TODO(dan): push out CHGHOST if vhost is applied
client.Send(nil, server.name, RPL_YOUREOPER, client.nick, "You are now an IRC operator") client.Send(nil, server.name, RPL_YOUREOPER, client.nick, "You are now an IRC operator")
//TODO(dan): Should this be sent automagically as part of setting the flag/mode? //TODO(dan): Should this be sent automagically as part of setting the flag/mode?
modech := ModeChanges{&ModeChange{ modech := ModeChanges{&ModeChange{
@ -906,6 +926,22 @@ func (server *Server) rehash() error {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file: %s", err.Error())
} }
// confirm operator stuff all exists and is fine
operclasses, err := config.OperatorClasses()
if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error())
}
opers, err := config.Operators(operclasses)
if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error())
}
for client := range server.currentOpers {
_, exists := opers[client.operName]
if !exists {
return fmt.Errorf("Oper [%s] no longer exists (used by client [%s])", client.operName, client.nickMaskString)
}
}
// setup new and removed caps // setup new and removed caps
addedCaps := make(CapabilitySet) addedCaps := make(CapabilitySet)
removedCaps := make(CapabilitySet) removedCaps := make(CapabilitySet)
@ -955,7 +991,8 @@ func (server *Server) rehash() error {
NickLen: int(config.Limits.NickLen), NickLen: int(config.Limits.NickLen),
TopicLen: int(config.Limits.TopicLen), TopicLen: int(config.Limits.TopicLen),
} }
server.operators = config.Operators() server.operclasses = *operclasses
server.operators = opers
server.checkIdent = config.Server.CheckIdent server.checkIdent = config.Server.CheckIdent
// registration // registration

View File

@ -59,10 +59,56 @@ registration:
enabled-callbacks: enabled-callbacks:
- none # no verification needed, will instantly register successfully - none # no verification needed, will instantly register successfully
# operator classes
oper-classes:
# local operator
"local-oper":
# title shown in WHOIS
title: Local Operator
# capability names
capabilities:
- "oper:local_kill"
- "oper:local_ban"
- "oper:local_unban"
# network operator
"network-oper":
# title shown in WHOIS
title: Network Operator
# oper class this extends from
extends: "local-oper"
# capability names
capabilities:
- "oper:remote_kill"
- "oper:remote_ban"
- "oper:remote_unban"
# server admin
"server-admin":
# title shown in WHOIS
title: Server Admin
# oper class this extends from
extends: "local-oper"
# capability names
capabilities:
- "oper:rehash"
- "oper:die"
# ircd operators # ircd operators
operator: opers:
# operator named 'dan' # operator named 'dan'
dan: dan:
# which capabilities this oper has access to
class: "server-admin"
# custom hostname
vhost: "n"
# password to login with /OPER command # password to login with /OPER command
# generated using "oragono genpasswd" # generated using "oragono genpasswd"
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu