From af3ceffdacf9e51c24d0d10e6f7921e267b69f7c Mon Sep 17 00:00:00 2001
From: Stefan Schubert <22217677+malaupa@users.noreply.github.com>
Date: Wed, 5 Jul 2023 19:50:07 +0200
Subject: [PATCH] add run-as to run hook in other user context

---
 docs/Hook-Definition.md |  1 +
 internal/hook/hook.go   |  1 +
 setuser.go              | 32 ++++++++++++++++++++++++++++++++
 setuser_windows.go      | 10 ++++++++++
 webhook.go              |  3 +++
 5 files changed, 47 insertions(+)
 create mode 100644 setuser.go
 create mode 100644 setuser_windows.go

diff --git a/docs/Hook-Definition.md b/docs/Hook-Definition.md
index 8a7e7443..d2754c12 100644
--- a/docs/Hook-Definition.md
+++ b/docs/Hook-Definition.md
@@ -6,6 +6,7 @@ Hooks are defined as objects in the JSON or YAML hooks configuration file. Pleas
 
  * `id` - specifies the ID of your hook. This value is used to create the HTTP endpoint (http://yourserver:port/hooks/your-hook-id)
  * `execute-command` - specifies the command that should be executed when the hook is triggered
+ * `run-as` - specifies a different user to run the command with
  * `command-working-directory` - specifies the working directory that will be used for the script when it's executed
  * `response-message` - specifies the string that will be returned to the hook initiator
  * `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
diff --git a/internal/hook/hook.go b/internal/hook/hook.go
index 05100957..4e343a76 100644
--- a/internal/hook/hook.go
+++ b/internal/hook/hook.go
@@ -566,6 +566,7 @@ func (h *HooksFiles) Set(value string) error {
 type Hook struct {
 	ID                                  string          `json:"id,omitempty"`
 	ExecuteCommand                      string          `json:"execute-command,omitempty"`
+	RunAs                               string          `json:"run-as,omitempty"`
 	CommandWorkingDirectory             string          `json:"command-working-directory,omitempty"`
 	ResponseMessage                     string          `json:"response-message,omitempty"`
 	ResponseHeaders                     ResponseHeaders `json:"response-headers,omitempty"`
diff --git a/setuser.go b/setuser.go
new file mode 100644
index 00000000..f6479f07
--- /dev/null
+++ b/setuser.go
@@ -0,0 +1,32 @@
+//go:build !windows
+// +build !windows
+
+package main
+
+import (
+	"log"
+	"os/exec"
+	"os/user"
+	"strconv"
+	"syscall"
+)
+
+// sets user for the command to execute
+func setUser(cmd *exec.Cmd, username string) {
+	user, err := user.Lookup(username)
+	if err != nil {
+		log.Printf("[%s] error lookup user: %s\n", username, err)
+		return
+	}
+	uid, err := strconv.ParseUint(user.Uid, 10, 32)
+	if err != nil {
+		log.Printf("Uid [%s] is not an decimal value: %s\n", user.Uid, err)
+		return
+	}
+	gid, err := strconv.ParseUint(user.Gid, 10, 32)
+	if err != nil {
+		log.Printf("Uid [%s] is not an decimal value: %s\n", user.Uid, err)
+		return
+	}
+	cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
+}
diff --git a/setuser_windows.go b/setuser_windows.go
new file mode 100644
index 00000000..1f50775f
--- /dev/null
+++ b/setuser_windows.go
@@ -0,0 +1,10 @@
+//go:build windows
+// +build windows
+
+package main
+
+import "os/exec"
+
+func setUser(cmd *exec.Cmd, username string) {
+	// NOOP: Windows doesn't have setuid setgid equivalent to the Unix world.
+}
diff --git a/webhook.go b/webhook.go
index d23cd028..8af90b5d 100644
--- a/webhook.go
+++ b/webhook.go
@@ -575,6 +575,9 @@ func handleHook(h *hook.Hook, r *hook.Request) (string, error) {
 	}
 
 	cmd := exec.Command(cmdPath)
+	if h.RunAs != "" {
+		setUser(cmd, h.RunAs)
+	}
 	cmd.Dir = h.CommandWorkingDirectory
 
 	cmd.Args, errors = h.ExtractCommandArguments(r)