From d293a2b9d6722dffde7998c953c3087e47a38a83 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Fri, 7 Sep 2018 04:31:29 +0100
Subject: [PATCH] Add sudo functionality to the API (#4809)

---
 docs/content/doc/advanced/api-usage.en-us.md |  4 ++
 integrations/api_admin_test.go               | 29 +++++++++++++
 routers/api/v1/api.go                        | 45 +++++++++++++++++++-
 templates/swagger/v1_json.tmpl               | 18 ++++++++
 4 files changed, 95 insertions(+), 1 deletion(-)

diff --git a/docs/content/doc/advanced/api-usage.en-us.md b/docs/content/doc/advanced/api-usage.en-us.md
index f04a99f14b..369bae6cac 100644
--- a/docs/content/doc/advanced/api-usage.en-us.md
+++ b/docs/content/doc/advanced/api-usage.en-us.md
@@ -73,3 +73,7 @@ using BasicAuth, as follows:
 $ curl --request GET --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens
 [{"name":"test","sha1":"..."},{"name":"dev","sha1":"..."}]
 ```
+
+## Sudo
+
+The API allows admin users to sudo API requests as another user. Simply add either a `sudo=` parameter or `Sudo:` request header with the username of the user to sudo.
diff --git a/integrations/api_admin_test.go b/integrations/api_admin_test.go
index 37e5fd199a..ab878dd6a5 100644
--- a/integrations/api_admin_test.go
+++ b/integrations/api_admin_test.go
@@ -9,6 +9,8 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"code.gitea.io/gitea/models"
 	api "code.gitea.io/sdk/gitea"
 )
@@ -71,3 +73,30 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
 		adminUsername, newPublicKey.ID)
 	session.MakeRequest(t, req, http.StatusForbidden)
 }
+
+func TestAPISudoUser(t *testing.T) {
+	prepareTestEnv(t)
+	adminUsername := "user1"
+	normalUsername := "user2"
+	session := loginUser(t, adminUsername)
+
+	urlStr := fmt.Sprintf("/api/v1/user?sudo=%s", normalUsername)
+	req := NewRequest(t, "GET", urlStr)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var user api.User
+	DecodeJSON(t, resp, &user)
+
+	assert.Equal(t, normalUsername, user.UserName)
+}
+
+func TestAPISudoUserForbidden(t *testing.T) {
+	prepareTestEnv(t)
+	adminUsername := "user1"
+	normalUsername := "user2"
+
+	session := loginUser(t, normalUsername)
+
+	urlStr := fmt.Sprintf("/api/v1/user?sudo=%s", adminUsername)
+	req := NewRequest(t, "GET", urlStr)
+	session.MakeRequest(t, req, http.StatusForbidden)
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 47a8edab43..967db3b01c 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -24,6 +24,8 @@
 //     - Token :
 //     - AccessToken :
 //     - AuthorizationHeaderToken :
+//     - SudoParam :
+//     - SudoHeader :
 //
 //     SecurityDefinitions:
 //     BasicAuth:
@@ -40,6 +42,16 @@
 //          type: apiKey
 //          name: Authorization
 //          in: header
+//     SudoParam:
+//          type: apiKey
+//          name: sudo
+//          in: query
+//          description: Sudo API request as the user provided as the key. Admin privileges are required.
+//     SudoHeader:
+//          type: apiKey
+//          name: Sudo
+//          in: header
+//          description: Sudo API request as the user provided as the key. Admin privileges are required.
 //
 // swagger:meta
 package v1
@@ -50,6 +62,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers/api/v1/admin"
 	"code.gitea.io/gitea/routers/api/v1/misc"
@@ -64,6 +77,36 @@ import (
 	"gopkg.in/macaron.v1"
 )
 
+func sudo() macaron.Handler {
+	return func(ctx *context.APIContext) {
+		sudo := ctx.Query("sudo")
+		if len(sudo) <= 0 {
+			sudo = ctx.Req.Header.Get("Sudo")
+		}
+
+		if len(sudo) > 0 {
+			if ctx.User.IsAdmin {
+				user, err := models.GetUserByName(sudo)
+				if err != nil {
+					if models.IsErrUserNotExist(err) {
+						ctx.Status(404)
+					} else {
+						ctx.Error(500, "GetUserByName", err)
+					}
+					return
+				}
+				log.Trace("Sudo from (%s) to: %s", ctx.User.Name, user.Name)
+				ctx.User = user
+			} else {
+				ctx.JSON(403, map[string]string{
+					"message": "Only administrators allowed to sudo.",
+				})
+				return
+			}
+		}
+	}
+}
+
 func repoAssignment() macaron.Handler {
 	return func(ctx *context.APIContext) {
 		userName := ctx.Params(":username")
@@ -589,5 +632,5 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Group("/topics", func() {
 			m.Get("/search", repo.TopicSearch)
 		})
-	}, context.APIContexter())
+	}, context.APIContexter(), sudo())
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 598813bfc0..b4b65563dc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -8008,6 +8008,18 @@
     "BasicAuth": {
       "type": "basic"
     },
+    "SudoHeader": {
+      "description": "Sudo API request as the user provided as the key. Admin privileges are required.",
+      "type": "apiKey",
+      "name": "Sudo",
+      "in": "header"
+    },
+    "SudoParam": {
+      "description": "Sudo API request as the user provided as the key. Admin privileges are required.",
+      "type": "apiKey",
+      "name": "sudo",
+      "in": "query"
+    },
     "Token": {
       "type": "apiKey",
       "name": "token",
@@ -8026,6 +8038,12 @@
     },
     {
       "AuthorizationHeaderToken": []
+    },
+    {
+      "SudoParam": []
+    },
+    {
+      "SudoHeader": []
     }
   ]
 }
\ No newline at end of file