service: Enable running service commands without users

This enables support for global service contexts, that are run
independently from a user context.

These contexts are considered to be admin. They only have access
to admin commands, because those are relevant in a global context.
This commit is contained in:
delthas 2023-01-19 18:33:22 +01:00 committed by Simon Ser
parent 4bd600c651
commit d17c7d57f2
3 changed files with 55 additions and 30 deletions

View File

@ -2433,6 +2433,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
nick: dc.nick, nick: dc.nick,
network: dc.network, network: dc.network,
user: dc.user, user: dc.user,
srv: dc.user.srv,
admin: dc.user.Admin, admin: dc.user.Admin,
print: func(text string) { print: func(text string) {
sendServicePRIVMSG(dc, text) sendServicePRIVMSG(dc, text)

View File

@ -39,7 +39,8 @@ type serviceContext struct {
context.Context context.Context
nick string // optional nick string // optional
network *network // optional network *network // optional
user *user user *user // optional
srv *Server
admin bool admin bool
print func(string) print func(string)
} }
@ -142,16 +143,26 @@ func handleServiceCommand(ctx *serviceContext, words []string) {
ctx.print("error: you must be an admin to use this command") ctx.print("error: you must be an admin to use this command")
return return
} }
if !cmd.admin && ctx.user == nil {
ctx.print("error: this command must be run as a user (try running with user run)")
return
}
if cmd.handle == nil { if cmd.handle == nil {
if len(cmd.children) > 0 { if len(cmd.children) > 0 {
var l []string var l []string
appendServiceCommandSetHelp(cmd.children, words, ctx.admin, &l) appendServiceCommandSetHelp(cmd.children, words, ctx.admin, ctx.user == nil, &l)
ctx.print("available commands: " + strings.Join(l, ", ")) ctx.print("available commands: " + strings.Join(l, ", "))
} else { } else {
// Pretend the command does not exist if it has neither children nor handler. // Pretend the command does not exist if it has neither children nor handler.
// This is obviously a bug but it is better to not die anyway. // This is obviously a bug but it is better to not die anyway.
ctx.user.logger.Printf("command without handler and subcommands invoked:", words[0]) var logger Logger
if ctx.user != nil {
logger = ctx.user.logger
} else {
logger = ctx.srv.Logger
}
logger.Printf("command without handler and subcommands invoked:", words[0])
ctx.print(fmt.Sprintf("command %q not found", words[0])) ctx.print(fmt.Sprintf("command %q not found", words[0]))
} }
return return
@ -340,18 +351,21 @@ func init() {
} }
} }
func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) { func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, global bool, l *[]string) {
for _, name := range cmds.Names() { for _, name := range cmds.Names() {
cmd := cmds[name] cmd := cmds[name]
if cmd.admin && !admin { if cmd.admin && !admin {
continue continue
} }
if !cmd.admin && global {
continue
}
words := append(prefix, name) words := append(prefix, name)
if len(cmd.children) == 0 { if len(cmd.children) == 0 {
s := strings.Join(words, " ") s := strings.Join(words, " ")
*l = append(*l, s) *l = append(*l, s)
} else { } else {
appendServiceCommandSetHelp(cmd.children, words, admin, l) appendServiceCommandSetHelp(cmd.children, words, admin, global, l)
} }
} }
} }
@ -366,7 +380,7 @@ func handleServiceHelp(ctx *serviceContext, params []string) error {
if len(cmd.children) > 0 { if len(cmd.children) > 0 {
var l []string var l []string
appendServiceCommandSetHelp(cmd.children, words, ctx.admin, &l) appendServiceCommandSetHelp(cmd.children, words, ctx.admin, ctx.user == nil, &l)
ctx.print("available commands: " + strings.Join(l, ", ")) ctx.print("available commands: " + strings.Join(l, ", "))
} else { } else {
text := strings.Join(words, " ") text := strings.Join(words, " ")
@ -379,7 +393,7 @@ func handleServiceHelp(ctx *serviceContext, params []string) error {
} }
} else { } else {
var l []string var l []string
appendServiceCommandSetHelp(serviceCommands, nil, ctx.admin, &l) appendServiceCommandSetHelp(serviceCommands, nil, ctx.admin, ctx.user == nil, &l)
ctx.print("available commands: " + strings.Join(l, ", ")) ctx.print("available commands: " + strings.Join(l, ", "))
} }
return nil return nil
@ -750,7 +764,7 @@ func handleServiceCertFPGenerate(ctx *serviceContext, params []string) error {
net.SASL.External.PrivKeyBlob = privKey net.SASL.External.PrivKeyBlob = privKey
net.SASL.Mechanism = "EXTERNAL" net.SASL.Mechanism = "EXTERNAL"
if err := ctx.user.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil { if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
return err return err
} }
@ -842,7 +856,7 @@ func handleServiceSASLSetPlain(ctx *serviceContext, params []string) error {
net.SASL.Plain.Password = fs.Arg(1) net.SASL.Plain.Password = fs.Arg(1)
net.SASL.Mechanism = "PLAIN" net.SASL.Mechanism = "PLAIN"
if err := ctx.user.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil { if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
return err return err
} }
@ -872,7 +886,7 @@ func handleServiceSASLReset(ctx *serviceContext, params []string) error {
net.SASL.External.PrivKeyBlob = nil net.SASL.External.PrivKeyBlob = nil
net.SASL.Mechanism = "" net.SASL.Mechanism = ""
if err := ctx.user.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil { if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
return err return err
} }
@ -885,15 +899,15 @@ func handleUserStatus(ctx *serviceContext, params []string) error {
// thousands of messages on large instances. // thousands of messages on large instances.
users := make([]database.User, 0, 50) users := make([]database.User, 0, 50)
ctx.user.srv.lock.Lock() ctx.srv.lock.Lock()
n := len(ctx.user.srv.users) n := len(ctx.srv.users)
for _, user := range ctx.user.srv.users { for _, user := range ctx.srv.users {
if len(users) == cap(users) { if len(users) == cap(users) {
break break
} }
users = append(users, user.User) users = append(users, user.User)
} }
ctx.user.srv.lock.Unlock() ctx.srv.lock.Unlock()
for _, user := range users { for _, user := range users {
var attrs []string var attrs []string
@ -908,7 +922,7 @@ func handleUserStatus(ctx *serviceContext, params []string) error {
if len(attrs) > 0 { if len(attrs) > 0 {
line += " (" + strings.Join(attrs, ", ") + ")" line += " (" + strings.Join(attrs, ", ") + ")"
} }
networks, err := ctx.user.srv.db.ListNetworks(ctx, user.ID) networks, err := ctx.srv.db.ListNetworks(ctx, user.ID)
if err != nil { if err != nil {
return fmt.Errorf("could not get networks of user %q: %v", user.Username, err) return fmt.Errorf("could not get networks of user %q: %v", user.Username, err)
} }
@ -960,7 +974,7 @@ func handleUserCreate(ctx *serviceContext, params []string) error {
return err return err
} }
} }
if _, err := ctx.user.srv.createUser(ctx, user); err != nil { if _, err := ctx.srv.createUser(ctx, user); err != nil {
return fmt.Errorf("could not create user: %v", err) return fmt.Errorf("could not create user: %v", err)
} }
@ -994,12 +1008,15 @@ func handleUserUpdate(ctx *serviceContext, params []string) error {
if fs.NArg() > 0 { if fs.NArg() > 0 {
return fmt.Errorf("unexpected argument: %v", fs.Arg(0)) return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
} }
if username == "" && ctx.user == nil {
return fmt.Errorf("cannot determine the user to update")
}
if password != nil && disablePassword { if password != nil && disablePassword {
return fmt.Errorf("flags -password and -disable-password are mutually exclusive") return fmt.Errorf("flags -password and -disable-password are mutually exclusive")
} }
if username != "" && username != ctx.user.Username { if username != "" && (ctx.user == nil || username != ctx.user.Username) {
if !ctx.admin { if !ctx.admin {
return fmt.Errorf("you must be an admin to update other users") return fmt.Errorf("you must be an admin to update other users")
} }
@ -1024,7 +1041,7 @@ func handleUserUpdate(ctx *serviceContext, params []string) error {
hashed = &hashedStr hashed = &hashedStr
} }
u := ctx.user.srv.getUser(username) u := ctx.srv.getUser(username)
if u == nil { if u == nil {
return fmt.Errorf("unknown username %q", username) return fmt.Errorf("unknown username %q", username)
} }
@ -1091,13 +1108,13 @@ func handleUserDelete(ctx *serviceContext, params []string) error {
hashBytes := sha1.Sum([]byte(username)) hashBytes := sha1.Sum([]byte(username))
hash := fmt.Sprintf("%x", hashBytes[0:3]) hash := fmt.Sprintf("%x", hashBytes[0:3])
self := ctx.user.Username == username self := ctx.user != nil && ctx.user.Username == username
if !ctx.admin && !self { if !ctx.admin && !self {
return fmt.Errorf("only admins may delete other users") return fmt.Errorf("only admins may delete other users")
} }
u := ctx.user.srv.getUser(username) u := ctx.srv.getUser(username)
if u == nil { if u == nil {
return fmt.Errorf("unknown username %q", username) return fmt.Errorf("unknown username %q", username)
} }
@ -1123,7 +1140,7 @@ func handleUserDelete(ctx *serviceContext, params []string) error {
return fmt.Errorf("failed to stop user: %v", err) return fmt.Errorf("failed to stop user: %v", err)
} }
if err := ctx.user.srv.db.DeleteUser(deleteCtx, u.ID); err != nil { if err := ctx.srv.db.DeleteUser(deleteCtx, u.ID); err != nil {
return fmt.Errorf("failed to delete user: %v", err) return fmt.Errorf("failed to delete user: %v", err)
} }
@ -1141,12 +1158,12 @@ func handleUserRun(ctx *serviceContext, params []string) error {
username := params[0] username := params[0]
params = params[1:] params = params[1:]
if username == ctx.user.Username { if ctx.user != nil && username == ctx.user.Username {
handleServiceCommand(ctx, params) handleServiceCommand(ctx, params)
return nil return nil
} }
u := ctx.user.srv.getUser(username) u := ctx.srv.getUser(username)
if u == nil { if u == nil {
return fmt.Errorf("unknown username %q", username) return fmt.Errorf("unknown username %q", username)
} }
@ -1384,7 +1401,7 @@ func handleServiceChannelUpdate(ctx *serviceContext, params []string) error {
network.conn.updateChannelAutoDetach(name) network.conn.updateChannelAutoDetach(name)
} }
if err := ctx.user.srv.db.StoreChannel(ctx, network.ID, ch); err != nil { if err := ctx.srv.db.StoreChannel(ctx, network.ID, ch); err != nil {
return fmt.Errorf("failed to update channel: %v", err) return fmt.Errorf("failed to update channel: %v", err)
} }
@ -1419,11 +1436,11 @@ func handleServiceChannelDelete(ctx *serviceContext, params []string) error {
} }
func handleServiceServerStatus(ctx *serviceContext, params []string) error { func handleServiceServerStatus(ctx *serviceContext, params []string) error {
dbStats, err := ctx.user.srv.db.Stats(ctx) dbStats, err := ctx.srv.db.Stats(ctx)
if err != nil { if err != nil {
return err return err
} }
serverStats := ctx.user.srv.Stats() serverStats := ctx.srv.Stats()
ctx.print(fmt.Sprintf("%v/%v users, %v downstreams, %v upstreams, %v networks, %v channels", serverStats.Users, dbStats.Users, serverStats.Downstreams, serverStats.Upstreams, dbStats.Networks, dbStats.Channels)) ctx.print(fmt.Sprintf("%v/%v users, %v downstreams, %v upstreams, %v networks, %v channels", serverStats.Users, dbStats.Users, serverStats.Downstreams, serverStats.Upstreams, dbStats.Networks, dbStats.Channels))
return nil return nil
} }
@ -1434,17 +1451,23 @@ func handleServiceServerNotice(ctx *serviceContext, params []string) error {
} }
text := params[0] text := params[0]
ctx.user.logger.Printf("broadcasting bouncer-wide NOTICE: %v", text) var logger Logger
if ctx.user != nil {
logger = ctx.user.logger
} else {
logger = ctx.srv.Logger
}
logger.Printf("broadcasting bouncer-wide NOTICE: %v", text)
broadcastMsg := &irc.Message{ broadcastMsg := &irc.Message{
Prefix: servicePrefix, Prefix: servicePrefix,
Command: "NOTICE", Command: "NOTICE",
Params: []string{"$" + ctx.user.srv.Config().Hostname, text}, Params: []string{"$" + ctx.srv.Config().Hostname, text},
} }
var err error var err error
sent := 0 sent := 0
total := 0 total := 0
ctx.user.srv.forEachUser(func(u *user) { ctx.srv.forEachUser(func(u *user) {
total++ total++
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -1454,7 +1477,7 @@ func handleServiceServerNotice(ctx *serviceContext, params []string) error {
} }
}) })
ctx.user.logger.Printf("broadcast bouncer-wide NOTICE to %v/%v downstreams", sent, total) logger.Printf("broadcast bouncer-wide NOTICE to %v/%v downstreams", sent, total)
ctx.print(fmt.Sprintf("sent to %v/%v downstream connections", sent, total)) ctx.print(fmt.Sprintf("sent to %v/%v downstream connections", sent, total))
return err return err

View File

@ -811,6 +811,7 @@ func (u *user) run() {
handleServiceCommand(&serviceContext{ handleServiceCommand(&serviceContext{
Context: ctx, Context: ctx,
user: u, user: u,
srv: u.srv,
admin: u.Admin, admin: u.Admin,
print: func(text string) { print: func(text string) {
// Avoid blocking on e.print in case our context is canceled. // Avoid blocking on e.print in case our context is canceled.