From d0a681fbc3fb626adcddbbb13f8c96c0bbd72c02 Mon Sep 17 00:00:00 2001
From: Romain <romdum@users.noreply.github.com>
Date: Tue, 12 Oct 2021 12:47:19 +0200
Subject: [PATCH] [API] Add endpount to get user org permissions (#17232)

* Add endpoint

* Add swagger response + generate swagger

* Stop execution if user / org is not found

* Add tests


Co-authored-by: 6543 <6543@obermui.de>
---
 integrations/api_user_org_perm_test.go | 149 +++++++++++++++++++++++++
 models/org.go                          |  13 +++
 modules/structs/org.go                 |   9 ++
 routers/api/v1/api.go                  |   5 +-
 routers/api/v1/org/org.go              |  71 ++++++++++++
 routers/api/v1/swagger/org.go          |   7 ++
 templates/swagger/v1_json.tmpl         |  72 ++++++++++++
 7 files changed, 325 insertions(+), 1 deletion(-)
 create mode 100644 integrations/api_user_org_perm_test.go

diff --git a/integrations/api_user_org_perm_test.go b/integrations/api_user_org_perm_test.go
new file mode 100644
index 0000000000..abba24701e
--- /dev/null
+++ b/integrations/api_user_org_perm_test.go
@@ -0,0 +1,149 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	api "code.gitea.io/gitea/modules/structs"
+	"github.com/stretchr/testify/assert"
+)
+
+type apiUserOrgPermTestCase struct {
+	LoginUser                       string
+	User                            string
+	Organization                    string
+	ExpectedOrganizationPermissions api.OrganizationPermissions
+}
+
+func TestTokenNeeded(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	session := emptyTestSession(t)
+	req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/user6/permissions")
+	session.MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
+	defer prepareTestEnv(t)()
+
+	session := loginUser(t, auoptc.LoginUser)
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/orgs/%s/permissions?token=%s", auoptc.User, auoptc.Organization, token))
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	var apiOP api.OrganizationPermissions
+	DecodeJSON(t, resp, &apiOP)
+	assert.Equal(t, auoptc.ExpectedOrganizationPermissions.IsOwner, apiOP.IsOwner)
+	assert.Equal(t, auoptc.ExpectedOrganizationPermissions.IsAdmin, apiOP.IsAdmin)
+	assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanWrite, apiOP.CanWrite)
+	assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanRead, apiOP.CanRead)
+	assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanCreateRepository, apiOP.CanCreateRepository)
+}
+
+func TestWithOwnerUser(t *testing.T) {
+	sampleTest(t, apiUserOrgPermTestCase{
+		LoginUser:    "user2",
+		User:         "user2",
+		Organization: "user3",
+		ExpectedOrganizationPermissions: api.OrganizationPermissions{
+			IsOwner:             true,
+			IsAdmin:             true,
+			CanWrite:            true,
+			CanRead:             true,
+			CanCreateRepository: true,
+		},
+	})
+}
+
+func TestCanWriteUser(t *testing.T) {
+	sampleTest(t, apiUserOrgPermTestCase{
+		LoginUser:    "user4",
+		User:         "user4",
+		Organization: "user3",
+		ExpectedOrganizationPermissions: api.OrganizationPermissions{
+			IsOwner:             false,
+			IsAdmin:             false,
+			CanWrite:            true,
+			CanRead:             true,
+			CanCreateRepository: false,
+		},
+	})
+}
+
+func TestAdminUser(t *testing.T) {
+	sampleTest(t, apiUserOrgPermTestCase{
+		LoginUser:    "user1",
+		User:         "user28",
+		Organization: "user3",
+		ExpectedOrganizationPermissions: api.OrganizationPermissions{
+			IsOwner:             false,
+			IsAdmin:             true,
+			CanWrite:            true,
+			CanRead:             true,
+			CanCreateRepository: true,
+		},
+	})
+}
+
+func TestAdminCanNotCreateRepo(t *testing.T) {
+	sampleTest(t, apiUserOrgPermTestCase{
+		LoginUser:    "user1",
+		User:         "user28",
+		Organization: "user6",
+		ExpectedOrganizationPermissions: api.OrganizationPermissions{
+			IsOwner:             false,
+			IsAdmin:             true,
+			CanWrite:            true,
+			CanRead:             true,
+			CanCreateRepository: false,
+		},
+	})
+}
+
+func TestCanReadUser(t *testing.T) {
+	sampleTest(t, apiUserOrgPermTestCase{
+		LoginUser:    "user1",
+		User:         "user24",
+		Organization: "org25",
+		ExpectedOrganizationPermissions: api.OrganizationPermissions{
+			IsOwner:             false,
+			IsAdmin:             false,
+			CanWrite:            false,
+			CanRead:             true,
+			CanCreateRepository: false,
+		},
+	})
+}
+
+func TestUnknowUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	session := loginUser(t, "user1")
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/unknow/orgs/org25/permissions?token=%s", token))
+	resp := session.MakeRequest(t, req, http.StatusNotFound)
+
+	var apiError api.APIError
+	DecodeJSON(t, resp, &apiError)
+	assert.Equal(t, "GetUserByName", apiError.Message)
+}
+
+func TestUnknowOrganization(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	session := loginUser(t, "user1")
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/user1/orgs/unknow/permissions?token=%s", token))
+	resp := session.MakeRequest(t, req, http.StatusNotFound)
+	var apiError api.APIError
+	DecodeJSON(t, resp, &apiError)
+	assert.Equal(t, "GetUserByName", apiError.Message)
+}
diff --git a/models/org.go b/models/org.go
index eadd1e157c..8cba485a89 100644
--- a/models/org.go
+++ b/models/org.go
@@ -392,6 +392,19 @@ func CanCreateOrgRepo(orgID, uid int64) (bool, error) {
 		Exist(new(Team))
 }
 
+// GetOrgUserMaxAuthorizeLevel returns highest authorize level of user in an organization
+func (org *User) GetOrgUserMaxAuthorizeLevel(uid int64) (AccessMode, error) {
+	var authorize AccessMode
+	_, err := db.GetEngine(db.DefaultContext).
+		Select("max(team.authorize)").
+		Table("team").
+		Join("INNER", "team_user", "team_user.team_id = team.id").
+		Where("team_user.uid = ?", uid).
+		And("team_user.org_id = ?", org.ID).
+		Get(&authorize)
+	return authorize, err
+}
+
 // GetUsersWhoCanCreateOrgRepo returns users which are able to create repo in organization
 func GetUsersWhoCanCreateOrgRepo(orgID int64) ([]*User, error) {
 	return getUsersWhoCanCreateOrgRepo(db.GetEngine(db.DefaultContext), orgID)
diff --git a/modules/structs/org.go b/modules/structs/org.go
index 38c6c6d6d8..4ae0ca8b6f 100644
--- a/modules/structs/org.go
+++ b/modules/structs/org.go
@@ -17,6 +17,15 @@ type Organization struct {
 	RepoAdminChangeTeamAccess bool   `json:"repo_admin_change_team_access"`
 }
 
+// OrganizationPermissions list differents users permissions on an organization
+type OrganizationPermissions struct {
+	IsOwner             bool `json:"is_owner"`
+	IsAdmin             bool `json:"is_admin"`
+	CanWrite            bool `json:"can_write"`
+	CanRead             bool `json:"can_read"`
+	CanCreateRepository bool `json:"can_create_repository"`
+}
+
 // CreateOrgOption options for creating an organization
 type CreateOrgOption struct {
 	// required: true
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 0a967e3c5a..d11bbf3c06 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -973,7 +973,10 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
 
 		// Organizations
 		m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
-		m.Get("/users/{username}/orgs", org.ListUserOrgs)
+		m.Group("/users/{username}/orgs", func() {
+			m.Get("", org.ListUserOrgs)
+			m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
+		})
 		m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
 		m.Get("/orgs", org.GetAll)
 		m.Group("/orgs/{org}", func() {
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index cf4c328ebb..d3aa92f46d 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -97,6 +97,77 @@ func ListUserOrgs(ctx *context.APIContext) {
 	listUserOrgs(ctx, u)
 }
 
+// GetUserOrgsPermissions get user permissions in organization
+func GetUserOrgsPermissions(ctx *context.APIContext) {
+	// swagger:operation GET /users/{username}/orgs/{org}/permissions organization orgGetUserPermissions
+	// ---
+	// summary: Get user permissions in organization
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: username
+	//   in: path
+	//   description: username of user
+	//   type: string
+	//   required: true
+	// - name: org
+	//   in: path
+	//   description: name of the organization
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/OrganizationPermissions"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	var u *models.User
+	if u = user.GetUserByParams(ctx); u == nil {
+		return
+	}
+
+	var o *models.User
+	if o = user.GetUserByParamsName(ctx, ":org"); o == nil {
+		return
+	}
+
+	op := api.OrganizationPermissions{}
+
+	if !models.HasOrgOrUserVisible(o, u) {
+		ctx.NotFound("HasOrgOrUserVisible", nil)
+		return
+	}
+
+	authorizeLevel, err := o.GetOrgUserMaxAuthorizeLevel(u.ID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetOrgUserAuthorizeLevel", err)
+		return
+	}
+
+	if authorizeLevel > models.AccessModeNone {
+		op.CanRead = true
+	}
+	if authorizeLevel > models.AccessModeRead {
+		op.CanWrite = true
+	}
+	if authorizeLevel > models.AccessModeWrite {
+		op.IsAdmin = true
+	}
+	if authorizeLevel > models.AccessModeAdmin {
+		op.IsOwner = true
+	}
+
+	op.CanCreateRepository, err = o.CanCreateOrgRepo(u.ID)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, op)
+}
+
 // GetAll return list of all public organizations
 func GetAll(ctx *context.APIContext) {
 	// swagger:operation Get /orgs organization orgGetAll
diff --git a/routers/api/v1/swagger/org.go b/routers/api/v1/swagger/org.go
index c962e7b188..d98e821ba7 100644
--- a/routers/api/v1/swagger/org.go
+++ b/routers/api/v1/swagger/org.go
@@ -35,3 +35,10 @@ type swaggerResponseTeamList struct {
 	// in:body
 	Body []api.Team `json:"body"`
 }
+
+// OrganizationPermissions
+// swagger:response OrganizationPermissions
+type swaggerResponseOrganizationPermissions struct {
+	// in:body
+	Body api.OrganizationPermissions `json:"body"`
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c6fa664af6..afb93c50fe 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -11856,6 +11856,45 @@
         }
       }
     },
+    "/users/{username}/orgs/{org}/permissions": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "organization"
+        ],
+        "summary": "Get user permissions in organization",
+        "operationId": "orgGetUserPermissions",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "username of user",
+            "name": "username",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the organization",
+            "name": "org",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/OrganizationPermissions"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/users/{username}/repos": {
       "get": {
         "produces": [
@@ -15877,6 +15916,33 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "OrganizationPermissions": {
+      "description": "OrganizationPermissions list differents users permissions on an organization",
+      "type": "object",
+      "properties": {
+        "can_create_repository": {
+          "type": "boolean",
+          "x-go-name": "CanCreateRepository"
+        },
+        "can_read": {
+          "type": "boolean",
+          "x-go-name": "CanRead"
+        },
+        "can_write": {
+          "type": "boolean",
+          "x-go-name": "CanWrite"
+        },
+        "is_admin": {
+          "type": "boolean",
+          "x-go-name": "IsAdmin"
+        },
+        "is_owner": {
+          "type": "boolean",
+          "x-go-name": "IsOwner"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "PRBranchInfo": {
       "description": "PRBranchInfo information about a branch",
       "type": "object",
@@ -17742,6 +17808,12 @@
         }
       }
     },
+    "OrganizationPermissions": {
+      "description": "OrganizationPermissions",
+      "schema": {
+        "$ref": "#/definitions/OrganizationPermissions"
+      }
+    },
     "PublicKey": {
       "description": "PublicKey",
       "schema": {