From d0a681fbc3fb626adcddbbb13f8c96c0bbd72c02 Mon Sep 17 00:00:00 2001 From: Romain 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 000000000000..abba24701ed1 --- /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 eadd1e157c92..8cba485a89fc 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 38c6c6d6d849..4ae0ca8b6f3c 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 0a967e3c5a78..d11bbf3c06c4 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 cf4c328ebbe8..d3aa92f46d1d 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 c962e7b188ee..d98e821ba744 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 c6fa664af6d9..afb93c50fe1b 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": {