delthas: 1 Enable user impersonation with SASL 2 files changed, 44 insertions(+), 13 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/soju-dev/patches/53206/mbox | git am -3Learn more about email & git
To impersonate a user, an admin user must login with SASL PLAIN, and pass a user(/network@device) in the SASL identity field. Impersonating a user works exactly like logging as that user, except for the following: - service commands are run as an admin - the enabled network limit is ignored (as for all admins) - the "idle" timestamps wrt user idle deactivation are not bumped --- downstream.go | 49 ++++++++++++++++++++++++++++++++++++++----------- user.go | 8 ++++++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/downstream.go b/downstream.go index 88eddcb..bc2e55e 100644 --- a/downstream.go +++ b/downstream.go @@ -292,7 +292,7 @@ var passthroughIsupport = map[string]bool{ } type saslPlain struct { - Username, Password string + Identity, Username, Password string } type downstreamSASL struct { @@ -333,10 +333,11 @@ type downstreamConn struct { id uint64 // These don't change after connection registration - registered bool - user *user - network *network // can be nil - clientName string + registered bool + user *user + network *network // can be nil + clientName string + impersonating bool nick string nickCM string @@ -663,6 +664,7 @@ func (dc *downstreamConn) handleMessageUnregistered(ctx context.Context, msg *ir } var username, clientName, networkName string + var impersonating bool switch credentials.mechanism { case "PLAIN": username, clientName, networkName = unmarshalUsername(credentials.plain.Username) @@ -678,6 +680,25 @@ func (dc *downstreamConn) handleMessageUnregistered(ctx context.Context, msg *ir err = fmt.Errorf("%v (username %q)", err, username) break } + + if credentials.plain.Identity != "" && credentials.plain.Identity != username { + var u *database.User + u, err = dc.srv.db.GetUser(ctx, username) + if err != nil { + err = fmt.Errorf("%v (username %q)", err, username) + break + } + if !u.Admin { + err = fmt.Errorf("SASL impersonation only allowed for admin users") + break + } + username, clientName, networkName = unmarshalUsername(credentials.plain.Identity) + if _, err = dc.srv.db.GetUser(ctx, username); err != nil { + err = fmt.Errorf("%v (username %q)", err, username) + break + } + impersonating = true + } case "OAUTHBEARER": auth, ok := dc.srv.Config().Auth.(auth.OAuthBearerAuthenticator) if !ok { @@ -711,6 +732,7 @@ func (dc *downstreamConn) handleMessageUnregistered(ctx context.Context, msg *ir panic(fmt.Errorf("username unset after SASL authentication")) } dc.setAuthUsername(username, clientName, networkName) + dc.impersonating = impersonating // Technically we should send RPL_LOGGEDIN here. However we use // RPL_LOGGEDIN to mirror the upstream connection status. Let's @@ -932,10 +954,8 @@ func (dc *downstreamConn) handleAuthenticate(ctx context.Context, msg *irc.Messa switch mech { case "PLAIN": server = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error { - if identity != "" && identity != username { - return fmt.Errorf("SASL PLAIN identity not supported") - } dc.sasl.plain = &saslPlain{ + Identity: identity, Username: username, Password: password, } @@ -2393,7 +2413,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. network: dc.network, user: dc.user, srv: dc.user.srv, - admin: dc.user.Admin, + admin: dc.user.Admin || dc.impersonating, print: func(text string) { sendServicePRIVMSG(dc, text) }, @@ -2488,6 +2508,13 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. switch credentials.mechanism { case "PLAIN": + if credentials.plain.Identity != "" && credentials.plain.Identity != credentials.plain.Username { + return ircError{&irc.Message{ + Command: irc.ERR_SASLFAIL, + Params: []string{dc.nick, "SASL PLAIN identity not supported"}, + }} + } + uc.logger.Printf("starting post-registration SASL PLAIN authentication with username %q", credentials.plain.Username) uc.saslClient = sasl.NewPlainClient("", credentials.plain.Username, credentials.plain.Password) uc.enqueueCommand(dc, &irc.Message{ @@ -3041,7 +3068,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. record.Realname = "" } - if record.Enabled { + if record.Enabled && !dc.impersonating { if err := dc.user.canEnableNewNetwork(ctx); err != nil { return ircError{&irc.Message{ Command: "FAIL", @@ -3094,7 +3121,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. record.Realname = "" } - if !wasEnabled && record.Enabled { + if !wasEnabled && record.Enabled && !dc.impersonating { if err := dc.user.canEnableNewNetwork(ctx); err != nil { return ircError{&irc.Message{ Command: "FAIL", diff --git a/user.go b/user.go index 0ad6dba..11c0275 100644 --- a/user.go +++ b/user.go @@ -768,7 +768,9 @@ func (u *user) run() { uc.updateAway() }) - u.bumpDownstreamInteractionTime(ctx) + if !dc.impersonating { + u.bumpDownstreamInteractionTime(ctx) + } case eventDownstreamDisconnected: dc := e.dc ctx := context.TODO() @@ -791,7 +793,9 @@ func (u *user) run() { uc.updateMonitor() }) - u.bumpDownstreamInteractionTime(ctx) + if !dc.impersonating { + u.bumpDownstreamInteractionTime(ctx) + } case eventDownstreamMessage: msg, dc := e.msg, e.dc if dc.isClosed() { base-commit: 9b54682fedfb5e2758b816a58f1f8e7668ba7acc -- 2.38.0