Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to query collaborators permission for a repository #18761

Merged
merged 13 commits into from
Apr 29, 2022
104 changes: 104 additions & 0 deletions integrations/api_repo_collaborator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2022 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 (
"net/http"
"net/url"
"testing"

"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIRepoCollaboratorPermission(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
//user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
user20 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}).(*user_model.User)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
repo1Owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID}).(*user_model.User)

// Login as User2.
session := loginUser(t, repo1Owner.Name)
testCtx := NewAPITestContext(t, repo1Owner.Name, repo1.Name)

t.Run("RepoOwnerShouldBeAdmin", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, repo1Owner.Name, testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "owner", repoPermission.Permission)
})

t.Run("CollaboratorWithReadAccess", func(t *testing.T) {
t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeRead))

req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, user4.Name, testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "read", repoPermission.Permission)
})

t.Run("CollaboratorWithWriteAccess", func(t *testing.T) {
t.Run("AddUserAsCollaboratorWithWriteAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeWrite))

req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, user4.Name, testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "write", repoPermission.Permission)
})

t.Run("CollaboratorWithAdminAccess", func(t *testing.T) {
t.Run("AddUserAsCollaboratorWithAdminAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeAdmin))

req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, user4.Name, testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "admin", repoPermission.Permission)
})

t.Run("WhyHasUser20ReadAccess", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, user20.Name, testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "read", repoPermission.Permission)
})

t.Run("WhyHasUser5ReadAccess", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, "user5", testCtx.Token)
resp := session.MakeRequest(t, req, http.StatusOK)

var repoPermission api.RepoCollaboratorPermission
DecodeJSON(t, resp, &repoPermission)

assert.Equal(t, "read", repoPermission.Permission)
})

t.Run("CollaboratorNotFound", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo1Owner.Name, repo1.Name, "non-existent-user", testCtx.Token)
session.MakeRequest(t, req, http.StatusNotFound)
})
})
}
9 changes: 9 additions & 0 deletions modules/convert/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,12 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
DiffViewStyle: user.DiffViewStyle,
}
}

// ToUserAndPermission return User and its collaboration permission for a repository
func ToUserAndPermission(user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
return api.RepoCollaboratorPermission{
User: ToUser(user, doer),
Permission: accessMode.String(),
RoleName: accessMode.String(),
}
}
7 changes: 7 additions & 0 deletions modules/structs/repo_collaborator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ package structs
type AddCollaboratorOption struct {
Permission *string `json:"permission"`
}

// RepoCollaboratorPermission to get repository permission for a collaborator
type RepoCollaboratorPermission struct {
Permission string `json:"permission"`
RoleName string `json:"role_name"`
User *User `json:"user"`
}
9 changes: 6 additions & 3 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,9 +801,12 @@ func Routes() *web.Route {
}, reqToken(), reqAdmin(), reqWebhooksEnabled())
m.Group("/collaborators", func() {
m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
m.Combo("/{collaborator}").Get(reqAnyRepoReader(), repo.IsCollaborator).
Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
Delete(reqAdmin(), repo.DeleteCollaborator)
m.Group("/{collaborator}", func() {
m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator).
Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
Delete(reqAdmin(), repo.DeleteCollaborator)
m.Get("/permission", repo.GetRepoPermissions)
}, reqToken())
}, reqToken())
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
Expand Down
56 changes: 56 additions & 0 deletions routers/api/v1/repo/collaborators.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,62 @@ func DeleteCollaborator(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}

// GetRepoPermissions gets repository permissions for a user
func GetRepoPermissions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator}/permission repository repoGetRepoPermissions
// ---
// summary: Get repository permissions for a user
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: collaborator
// in: path
// description: username of the collaborator
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RepoCollaboratorPermission"
// "404":
// "$ref": "#/responses/notFound"
// "403":
// "$ref": "#/responses/forbidden"

// only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own
if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.Params(":collaborator") && !ctx.IsUserRepoAdmin() {
ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
return
}

collaborator, err := user_model.GetUserByName(ctx.Params(":collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusNotFound, "GetUserByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
}
return
}

permission, err := models.GetUserRepoPermission(ctx.Repo.Repository, collaborator)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
}

ctx.JSON(http.StatusOK, convert.ToUserAndPermission(collaborator, ctx.ContextUser, permission.AccessMode))
}

// GetReviewers return all users that can be requested to review in this repo
func GetReviewers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
Expand Down
7 changes: 7 additions & 0 deletions routers/api/v1/swagger/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,10 @@ type swaggerWikiCommitList struct {
// in:body
Body api.WikiCommitList `json:"body"`
}

// RepoCollaboratorPermission
// swagger:response RepoCollaboratorPermission
type swaggerRepoCollaboratorPermission struct {
// in:body
Body api.RepoCollaboratorPermission `json:"body"`
}
70 changes: 70 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3129,6 +3129,52 @@
}
}
},
"/repos/{owner}/{repo}/collaborators/{collaborator}/permission": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get repository permissions for a user",
"operationId": "repoGetRepoPermissions",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "username of the collaborator",
"name": "collaborator",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/RepoCollaboratorPermission"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/commits": {
"get": {
"produces": [
Expand Down Expand Up @@ -17443,6 +17489,24 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"RepoCollaboratorPermission": {
"description": "RepoCollaboratorPermission to get repository permission for a collaborator",
"type": "object",
"properties": {
"permission": {
"type": "string",
"x-go-name": "Permission"
},
"role_name": {
"type": "string",
"x-go-name": "RoleName"
},
"user": {
"$ref": "#/definitions/User"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"RepoCommit": {
"type": "object",
"title": "RepoCommit contains information of a commit in the context of a repository.",
Expand Down Expand Up @@ -19118,6 +19182,12 @@
}
}
},
"RepoCollaboratorPermission": {
"description": "RepoCollaboratorPermission",
"schema": {
"$ref": "#/definitions/RepoCollaboratorPermission"
}
},
"Repository": {
"description": "Repository",
"schema": {
Expand Down