mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-23 13:55:27 +00:00
de484e86bc
This PR adds the support for scopes of access tokens, mimicking the design of GitHub OAuth scopes. The changes of the core logic are in `models/auth` that `AccessToken` struct will have a `Scope` field. The normalized (no duplication of scope), comma-separated scope string will be stored in `access_token` table in the database. In `services/auth`, the scope will be stored in context, which will be used by `reqToken` middleware in API calls. Only OAuth2 tokens will have granular token scopes, while others like BasicAuth will default to scope `all`. A large amount of work happens in `routers/api/v1/api.go` and the corresponding `tests/integration` tests, that is adding necessary scopes to each of the API calls as they fit. - [x] Add `Scope` field to `AccessToken` - [x] Add access control to all API endpoints - [x] Update frontend & backend for when creating tokens - [x] Add a database migration for `scope` column (enable 'all' access to past tokens) I'm aiming to complete it before Gitea 1.19 release. Fixes #4300
227 lines
8.8 KiB
Go
227 lines
8.8 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/services/forms"
|
|
issue_service "code.gitea.io/gitea/services/issue"
|
|
"code.gitea.io/gitea/tests"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestAPIViewPulls(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
|
|
ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeRepo)
|
|
|
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&token="+ctx.Token, owner.Name, repo.Name)
|
|
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
var pulls []*api.PullRequest
|
|
DecodeJSON(t, resp, &pulls)
|
|
expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
|
|
assert.Len(t, pulls, expectedLen)
|
|
|
|
pull := pulls[0]
|
|
if assert.EqualValues(t, 5, pull.ID) {
|
|
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
|
|
_, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
// TODO: use diff to generate stats to test against
|
|
|
|
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
|
|
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
|
|
if assert.Len(t, files, 1) {
|
|
assert.EqualValues(t, "File-WoW", files[0].Filename)
|
|
assert.EqualValues(t, "", files[0].PreviousFilename)
|
|
assert.EqualValues(t, 1, files[0].Additions)
|
|
assert.EqualValues(t, 1, files[0].Changes)
|
|
assert.EqualValues(t, 0, files[0].Deletions)
|
|
assert.EqualValues(t, "added", files[0].Status)
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
// TestAPIMergePullWIP ensures that we can't merge a WIP pull request
|
|
func TestAPIMergePullWIP(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false))
|
|
pr.LoadIssue(db.DefaultContext)
|
|
issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
|
|
|
|
// force reload
|
|
pr.LoadAttributes(db.DefaultContext)
|
|
|
|
assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner.Name, repo.Name, pr.Index, token), &forms.MergePullRequestForm{
|
|
MergeMessageField: pr.Issue.Title,
|
|
Do: string(repo_model.MergeStyleMerge),
|
|
})
|
|
|
|
MakeRequest(t, req, http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
func TestAPICreatePullSuccess(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
|
// repo10 have code, pulls units.
|
|
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
|
// repo11 only have code unit but should still create pulls
|
|
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
|
|
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
|
|
|
|
session := loginUser(t, owner11.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
|
|
Head: fmt.Sprintf("%s:master", owner11.Name),
|
|
Base: "master",
|
|
Title: "create a failure pr",
|
|
})
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
|
|
}
|
|
|
|
func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
// repo10 have code, pulls units.
|
|
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
|
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
|
|
// repo11 only have code unit but should still create pulls
|
|
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
|
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
|
|
|
|
session := loginUser(t, owner11.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
|
|
|
opts := &api.CreatePullRequestOption{
|
|
Head: fmt.Sprintf("%s:master", owner11.Name),
|
|
Base: "master",
|
|
Title: "create a failure pr",
|
|
Body: "foobaaar",
|
|
Milestone: 5,
|
|
Assignees: []string{owner10.Name},
|
|
Labels: []int64{5},
|
|
}
|
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), opts)
|
|
|
|
res := MakeRequest(t, req, http.StatusCreated)
|
|
pull := new(api.PullRequest)
|
|
DecodeJSON(t, res, pull)
|
|
|
|
assert.NotNil(t, pull.Milestone)
|
|
assert.EqualValues(t, opts.Milestone, pull.Milestone.ID)
|
|
if assert.Len(t, pull.Assignees, 1) {
|
|
assert.EqualValues(t, opts.Assignees[0], owner10.Name)
|
|
}
|
|
assert.NotNil(t, pull.Labels)
|
|
assert.EqualValues(t, opts.Labels[0], pull.Labels[0].ID)
|
|
}
|
|
|
|
func TestAPICreatePullWithFieldsFailure(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
// repo10 have code, pulls units.
|
|
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
|
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
|
|
// repo11 only have code unit but should still create pulls
|
|
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
|
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
|
|
|
|
session := loginUser(t, owner11.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
|
|
|
opts := &api.CreatePullRequestOption{
|
|
Head: fmt.Sprintf("%s:master", owner11.Name),
|
|
Base: "master",
|
|
}
|
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), opts)
|
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
|
opts.Title = "is required"
|
|
|
|
opts.Milestone = 666
|
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
|
opts.Milestone = 5
|
|
|
|
opts.Assignees = []string{"qweruqweroiuyqweoiruywqer"}
|
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
|
opts.Assignees = []string{owner10.LoginName}
|
|
|
|
opts.Labels = []int64{55555}
|
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
|
opts.Labels = []int64{5}
|
|
}
|
|
|
|
func TestAPIEditPull(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
|
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
|
|
|
|
session := loginUser(t, owner10.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
|
|
Head: "develop",
|
|
Base: "master",
|
|
Title: "create a success pr",
|
|
})
|
|
pull := new(api.PullRequest)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
DecodeJSON(t, resp, pull)
|
|
assert.EqualValues(t, "master", pull.Base.Name)
|
|
|
|
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s", owner10.Name, repo10.Name, pull.Index, token), &api.EditPullRequestOption{
|
|
Base: "feature/1",
|
|
Title: "edit a this pr",
|
|
})
|
|
resp = MakeRequest(t, req, http.StatusCreated)
|
|
DecodeJSON(t, resp, pull)
|
|
assert.EqualValues(t, "feature/1", pull.Base.Name)
|
|
|
|
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s", owner10.Name, repo10.Name, pull.Index, token), &api.EditPullRequestOption{
|
|
Base: "not-exist",
|
|
})
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/files?token=%s", ctx.Username, ctx.Reponame, pr.Index, ctx.Token)
|
|
|
|
req := NewRequest(t, http.MethodGet, url)
|
|
if ctx.ExpectedCode == 0 {
|
|
ctx.ExpectedCode = http.StatusOK
|
|
}
|
|
resp := ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
|
|
|
files := make([]*api.ChangedFile, 0, 1)
|
|
DecodeJSON(t, resp, &files)
|
|
|
|
if callback != nil {
|
|
callback(t, files)
|
|
}
|
|
}
|
|
}
|