diff --git a/CHANGELOG.md b/CHANGELOG.md index 79eea500..0fa6fee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ New release of Oragono! ### Security ### Added +* Operator classes, allowing for more finely-grained permissions for operators. ### Changed +* In the config file, "operator" changed to "opers", and new oper class is required. ### Removed diff --git a/irc/client.go b/irc/client.go index dfea9e3f..937cf1a7 100644 --- a/irc/client.go +++ b/irc/client.go @@ -37,6 +37,7 @@ type Client struct { capVersion CapVersion certfp string channels ChannelSet + class *OperClass ctime time.Time flags map[UserMode]bool isDestroyed bool @@ -50,6 +51,7 @@ type Client struct { nickCasefolded string nickMaskString string // cache for nickmask string since it's used with lots of replies nickMaskCasefolded string + operName string quitTimer *time.Timer realname string registered bool @@ -347,6 +349,12 @@ func (client *Client) destroy() { friends := client.Friends() friends.Remove(client) + // remove from opers list + _, exists := client.server.currentOpers[client] + if exists { + delete(client.server.currentOpers, client) + } + // alert monitors for _, mClient := range client.server.monitoring[client.nickCasefolded] { mClient.Send(nil, client.server.name, RPL_MONOFFLINE, mClient.nick, client.nick) diff --git a/irc/config.go b/irc/config.go index accb5f84..60da5532 100644 --- a/irc/config.go +++ b/irc/config.go @@ -8,6 +8,7 @@ package irc import ( "crypto/tls" "errors" + "fmt" "io/ioutil" "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 { Network struct { Name string @@ -92,7 +113,9 @@ type Config struct { Accounts AccountRegistrationConfig } - Operator map[string]*PassConfig + OperClasses map[string]*OperClassConfig `yaml:"oper-classes"` + + Opers map[string]*OperConfig Limits struct { NickLen uint `yaml:"nicklen"` @@ -105,17 +128,97 @@ type Config struct { } } -func (conf *Config) Operators() map[string][]byte { - operators := make(map[string][]byte) - for name, opConf := range conf.Operator { - name, err := CasefoldName(name) - if err == nil { - operators[name] = opConf.PasswordBytes() - } else { - log.Println("Could not casefold oper name:", err.Error()) +type OperClass struct { + Title string + Capabilities map[string]bool // map to make lookups much easier +} + +func (conf *Config) OperatorClasses() (*map[string]OperClass, error) { + ocs := make(map[string]OperClass) + + // 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 { @@ -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 { return nil, errors.New("Limits aren't setup properly, check them and make them sane") } + return config, nil } diff --git a/irc/server.go b/irc/server.go index 22b7c31b..6c979a28 100644 --- a/irc/server.go +++ b/irc/server.go @@ -66,6 +66,7 @@ type Server struct { commands chan Command configFilename string ctime time.Time + currentOpers map[*Client]bool store buntdb.DB idle chan *Client limits Limits @@ -78,7 +79,8 @@ type Server struct { nameCasefolded string networkName string newConns chan clientConn - operators map[string][]byte + operators map[string]Oper + operclasses map[string]OperClass password []byte passwords *PasswordManager rehashMutex sync.Mutex @@ -115,6 +117,15 @@ func NewServer(configFilename string, config *Config) *Server { 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{ accounts: make(map[string]*ClientAccount), authenticationEnabled: config.AuthenticationEnabled, @@ -123,6 +134,7 @@ func NewServer(configFilename string, config *Config) *Server { commands: make(chan Command), configFilename: configFilename, ctime: time.Now(), + currentOpers: make(map[*Client]bool), idle: make(chan *Client), limits: Limits{ AwayLen: int(config.Limits.AwayLen), @@ -138,7 +150,8 @@ func NewServer(configFilename string, config *Config) *Server { nameCasefolded: casefoldedName, networkName: config.Network.Name, newConns: make(chan clientConn), - operators: config.Operators(), + operclasses: *operClasses, + operators: opers, signals: make(chan os.Signal, len(SERVER_SIGNALS)), rehashSignal: make(chan os.Signal, 1), 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") return true } - hash := server.operators[name] + hash := server.operators[name].Pass password := []byte(msg.Params[1]) err = ComparePassword(hash, password) if (hash == nil) || (err != nil) { + fmt.Println("2", hash) client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, "Password incorrect") return 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") //TODO(dan): Should this be sent automagically as part of setting the flag/mode? modech := ModeChanges{&ModeChange{ @@ -906,6 +926,22 @@ func (server *Server) rehash() 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 addedCaps := make(CapabilitySet) removedCaps := make(CapabilitySet) @@ -955,7 +991,8 @@ func (server *Server) rehash() error { NickLen: int(config.Limits.NickLen), TopicLen: int(config.Limits.TopicLen), } - server.operators = config.Operators() + server.operclasses = *operclasses + server.operators = opers server.checkIdent = config.Server.CheckIdent // registration diff --git a/oragono.yaml b/oragono.yaml index 02efb3ab..ee452687 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -59,10 +59,56 @@ registration: enabled-callbacks: - 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 -operator: +opers: # operator named 'dan' dan: + # which capabilities this oper has access to + class: "server-admin" + + # custom hostname + vhost: "n" + # password to login with /OPER command # generated using "oragono genpasswd" password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu