~emersion/soju-dev

Enable user impersonation with SASL v1 PROPOSED

delthas: 1
 Enable user impersonation with SASL

 2 files changed, 44 insertions(+), 13 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH] Enable user impersonation with SASL Export this patch

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