service: add user run

This enables to run commands as other users, like sudo.

This is useful for eg fixing a user networks on their behalf.
This commit is contained in:
delthas 2023-01-17 14:03:13 +01:00 committed by Simon Ser
parent e7a06fe208
commit b29c9ef09a
2 changed files with 81 additions and 0 deletions

View File

@ -128,7 +128,10 @@ func handleServicePRIVMSG(ctx *serviceContext, text string) {
ctx.print(fmt.Sprintf(`error: failed to parse command: %v`, err)) ctx.print(fmt.Sprintf(`error: failed to parse command: %v`, err))
return return
} }
handleServiceCommand(ctx, words)
}
func handleServiceCommand(ctx *serviceContext, words []string) {
cmd, params, err := serviceCommands.Get(words) cmd, params, err := serviceCommands.Get(words)
if err != nil { if err != nil {
ctx.print(fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err)) ctx.print(fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
@ -285,6 +288,12 @@ func init() {
desc: "delete a user", desc: "delete a user",
handle: handleUserDelete, handle: handleUserDelete,
}, },
"run": {
usage: "<username> <command>",
desc: "run a command as another user",
handle: handleUserRun,
admin: true,
},
}, },
}, },
"channel": { "channel": {
@ -1047,6 +1056,57 @@ func handleUserDelete(ctx *serviceContext, params []string) error {
return nil return nil
} }
func handleUserRun(ctx *serviceContext, params []string) error {
if !ctx.user.Admin {
return fmt.Errorf("only admins may run command as other users")
}
if len(params) < 2 {
return fmt.Errorf("expected at least two arguments")
}
username := params[0]
params = params[1:]
if username == ctx.user.Username {
handleServiceCommand(ctx, params)
return nil
}
u := ctx.user.srv.getUser(username)
if u == nil {
return fmt.Errorf("unknown username %q", username)
}
printCh := make(chan string, 1)
ev := eventUserRun{
params: params,
print: printCh,
}
select {
case <-ctx.Done():
return ctx.Err()
case u.events <- ev:
}
for {
select {
case <-ctx.Done():
// This handles a possible race condition:
// - we send ev to u.events
// - the user goroutine for u stops (because of a crash or user deletion)
// - we would block on printCh
// Quitting on ctx.Done() prevents us from blocking indefinitely
// in case the event is never processed.
// TODO: Properly fix this condition by flushing the u.events queue
// and running close(ev.print) in a defer
return nil
case text, ok := <-printCh:
if !ok {
return nil
}
ctx.print(text)
}
}
}
func handleServiceChannelStatus(ctx *serviceContext, params []string) error { func handleServiceChannelStatus(ctx *serviceContext, params []string) error {
var defaultNetworkName string var defaultNetworkName string
if ctx.network != nil { if ctx.network != nil {

21
user.go
View File

@ -82,6 +82,11 @@ type eventTryRegainNick struct {
nick string nick string
} }
type eventUserRun struct {
params []string
print chan string
}
type deliveredClientMap map[string]string // client name -> msg ID type deliveredClientMap map[string]string // client name -> msg ID
type deliveredStore struct { type deliveredStore struct {
@ -769,6 +774,22 @@ func (u *user) run() {
} }
case eventTryRegainNick: case eventTryRegainNick:
e.uc.tryRegainNick(e.nick) e.uc.tryRegainNick(e.nick)
case eventUserRun:
ctx := context.TODO()
handleServiceCommand(&serviceContext{
Context: ctx,
user: u,
print: func(text string) {
// Avoid blocking on e.print in case our context is canceled.
// This is a no-op right now because we use context.TODO(),
// but might be useful later when we add timeouts.
select {
case <-ctx.Done():
case e.print <- text:
}
},
}, e.params)
close(e.print)
case eventStop: case eventStop:
for _, dc := range u.downstreamConns { for _, dc := range u.downstreamConns {
dc.Close() dc.Close()