mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 06:20:24 +00:00
feat: ensure repo-specific access tokens can't perform repo admin operations (#11736)
Last known backend change for #11311, fixing up some loose ends on the repository APIs related to repo-specific access tokens. Adds automated testing, and aligns permissions where necessary, to ensure that repo-specific access tokens can't change the administrative state of the repositories that they are limited to. Repo-specific access tokens cannot be used to: - convert a mirror into a normal repo, - create a new repository from a template, - transfer ownership of a repository - create a new repository (already protected, but test automation added), - delete a repository (already protected, but test automation added), - editing a repository's settings (already protected, but test automation added). **Breaking**: The template generation (`POST /repos/{template_owner}/{template_repo}/generate`) and repository deletion (`DELETE /repos/{username}/{reponame}`) APIs have been updated to require the same permission scope as creating a new repository. Either `write:user` or `write:organization` is required, depending on the owner of the repository being created or deleted. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11736 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
parent
dc65408618
commit
a27f9a719e
11 changed files with 264 additions and 53 deletions
1
release-notes/11736.md
Normal file
1
release-notes/11736.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
The template generation (`POST /repos/{template_owner}/{template_repo}/generate`) and repository deletion (`DELETE /repos/{username}/{reponame}`) APIs have been updated to require the same permission scope as creating a new repository. Either `write:user` or `write:organization` is required, depending on the owner of the repository being created or deleted.
|
||||
|
|
@ -358,6 +358,16 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
|||
}
|
||||
}
|
||||
|
||||
// Middleware that dynamically checks either the organization or user scope, depending on the owner type of the
|
||||
// repository (requires `repoAssignment()` middleware to be used before this).
|
||||
func tokenRequiresRepoOwnerScope(ctx *context.APIContext) {
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)(ctx)
|
||||
} else {
|
||||
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Contexter middleware already checks token for user sign in process.
|
||||
func reqToken() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
|
|
@ -1109,6 +1119,10 @@ func Routes() *web.Route {
|
|||
// FIXME: Don't expose repository id outside of the system
|
||||
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)
|
||||
|
||||
// Needs to be extracted from the larger `/repos` group because deleting a repo isn't protected by
|
||||
// `AccessTokenScopeCategoryRepository`; it's protected by either the User or Organization scope.
|
||||
m.Delete("/repos/{username}/{reponame}", repoAssignment(), tokenRequiresRepoOwnerScope, reqOwner(), repo.Delete)
|
||||
|
||||
// Repos (requires repo scope)
|
||||
m.Group("/repos", func() {
|
||||
m.Get("/search", repo.Search)
|
||||
|
|
@ -1120,12 +1134,12 @@ func Routes() *web.Route {
|
|||
m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
|
||||
|
||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
|
||||
Delete(reqToken(), reqOwner(), repo.Delete).
|
||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
||||
m.Post("/convert", reqOwner(), repo.Convert)
|
||||
|
||||
m.Post("/convert", reqOwner(), reqAdmin(), repo.Convert)
|
||||
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
||||
m.Group("/transfer", func() {
|
||||
m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
|
||||
m.Post("", reqOwner(), reqAdmin(), bind(api.TransferRepoOption{}), repo.Transfer)
|
||||
m.Post("/accept", repo.AcceptTransfer)
|
||||
m.Post("/reject", repo.RejectTransfer)
|
||||
}, reqToken())
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
activities_model "forgejo.org/models/activities"
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
|
|
@ -404,10 +405,10 @@ func Generate(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
ctxUser := ctx.Doer
|
||||
targetOwner := ctx.Doer
|
||||
var err error
|
||||
if form.Owner != ctxUser.Name {
|
||||
ctxUser, err = user_model.GetUserByName(ctx, form.Owner)
|
||||
if form.Owner != targetOwner.Name {
|
||||
targetOwner, err = user_model.GetUserByName(ctx, form.Owner)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{
|
||||
|
|
@ -420,13 +421,13 @@ func Generate(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.IsUserSiteAdmin() && !ctxUser.IsOrganization() {
|
||||
if !ctx.IsUserSiteAdmin() && !targetOwner.IsOrganization() {
|
||||
ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.")
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
canCreate, err := organization.OrgFromUser(targetOwner).CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
return
|
||||
|
|
@ -435,13 +436,23 @@ func Generate(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
context.CheckRuntimeDeterminedScope(ctx, auth_model.AccessTokenScopeCategoryOrganization, auth_model.Write, "token requires scope write:organization to create a repository owned by a user")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
context.CheckRuntimeDeterminedScope(ctx, auth_model.AccessTokenScopeCategoryUser, auth_model.Write, "token requires scope write:user to create a repository owned by a user")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
||||
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, targetOwner.ID, targetOwner.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
|
||||
repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, targetOwner, ctx.Repo.Repository, opts)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
|
||||
|
|
@ -453,7 +464,7 @@ func Generate(ctx *context.APIContext) {
|
|||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
|
||||
log.Trace("Repository generated [%d]: %s/%s", repo.ID, targetOwner.Name, repo.Name)
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,3 +185,23 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CheckRuntimeDeterminedScope(ctx *APIContext, scopeCategory auth_model.AccessTokenScopeCategory, level auth_model.AccessTokenScopeLevel, msg string) {
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if ok {
|
||||
var scopeMatched bool
|
||||
|
||||
requiredScopes := auth_model.GetRequiredScopes(level, scopeCategory)
|
||||
|
||||
scopeMatched, err := scope.HasScope(requiredScopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !scopeMatched {
|
||||
ctx.Error(http.StatusForbidden, "!scopeMatched", msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ jobs:
|
|||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteUser)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ jobs:
|
|||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteUser)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ jobs:
|
|||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
|
@ -620,7 +620,7 @@ func TestActionsEphemeral(t *testing.T) {
|
|||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsEphemeralRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ jobs:
|
|||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteUser)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
|
@ -275,7 +275,7 @@ jobs:
|
|||
)
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteUser)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||
assert.Equal(t, testCase.notifyEmail, actionRun.NotifyEmail)
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteUser)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,3 +64,19 @@ func TestAPIConvert(t *testing.T) {
|
|||
repo4edited := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.False(t, repo4edited.IsMirror)
|
||||
}
|
||||
|
||||
// This test verifies that a repo-specific access token with `write:repository` scope is not a sufficient scope to edit
|
||||
// the settings of a repository that is within its repo-specific list.
|
||||
func TestAPIConvertAccessTokenResources(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo5 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
|
||||
org3 := "org3"
|
||||
|
||||
repoSpecificToken := createFineGrainedRepoAccessToken(t, "user2",
|
||||
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteRepository},
|
||||
[]int64{repo5.ID},
|
||||
)
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/convert", org3, repo5.Name)).AddTokenAuth(repoSpecificToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,3 +394,21 @@ func TestAPIRepoEdit(t *testing.T) {
|
|||
assert.Equal(t, "rebase", apiRepo.DefaultUpdateStyle)
|
||||
})
|
||||
}
|
||||
|
||||
// This test verifies that a repo-specific access token with `write:repository` scope is not a sufficient scope to edit
|
||||
// the settings of a repository that is within its repo-specific list.
|
||||
func TestAPIRepoEditAccessTokenResources(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
|
||||
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteRepository},
|
||||
[]int64{2},
|
||||
)
|
||||
desc := "here's a new description"
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/user2/repo2"),
|
||||
&api.EditRepoOption{
|
||||
Description: &desc,
|
||||
}).
|
||||
AddTokenAuth(repo2OnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -824,6 +824,75 @@ func testAPIRepoCreateConflict(t *testing.T, u *url.URL) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoCreateDenied(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// This test verifies that `write:repository` is not a sufficient scope to create a repository. If it was, then
|
||||
// repo-specific access tokens would be able to create new repositories.
|
||||
session := loginUser(t, "user2")
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos",
|
||||
&api.CreateRepoOption{
|
||||
Name: "my-new-repo",
|
||||
}).
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPIRepoDelete(t *testing.T) {
|
||||
t.Run("permitted to delete user repo w/ user scope", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo2").
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
})
|
||||
|
||||
t.Run("denied to delete user repo w/ org scope", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo2").
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token does not have at least one of required scope(s): [write:user]")
|
||||
})
|
||||
|
||||
t.Run("permitted to delete org repo w/ org scope", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/org3/repo3").
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
})
|
||||
|
||||
t.Run("denied to delete org repo w/ user scope", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/org3/repo3").
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token does not have at least one of required scope(s): [write:organization]")
|
||||
})
|
||||
|
||||
t.Run("denied with repo-specific", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
// limit ourselves to write:repository -- repo-specific access tokens can't be created with write:user
|
||||
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
|
||||
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteRepository},
|
||||
[]int64{2},
|
||||
)
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo2").
|
||||
AddTokenAuth(repo2OnlyToken)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token does not have at least one of required scope(s): [write:user]")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoTransfer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
ctxUserID int64
|
||||
|
|
@ -884,6 +953,23 @@ func TestAPIRepoTransfer(t *testing.T) {
|
|||
_ = repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID)
|
||||
}
|
||||
|
||||
// This test verifies that a repo-specific access token with `write:repository` scope is not a sufficient to transfer a
|
||||
// repository to another user.
|
||||
func TestAPIRepoTransferAccessTokenResources(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
|
||||
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeWriteRepository},
|
||||
[]int64{2},
|
||||
)
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo2/transfer", &api.TransferRepoOption{
|
||||
NewOwner: "org3",
|
||||
}).AddTokenAuth(repo2OnlyToken)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "user should be an owner or a collaborator with admin write")
|
||||
}
|
||||
|
||||
func transfer(t *testing.T) *repo_model.Repository {
|
||||
// create repo to move
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
|
@ -972,38 +1058,93 @@ func TestAPIRejectTransfer(t *testing.T) {
|
|||
func TestAPIGenerateRepo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
|
||||
// write:repository scope is always required (logically, because we're writing inside the contents of a new
|
||||
// repository) but the need for write:user or write:organization depends on the target owner, so we'll test those
|
||||
// combinations.
|
||||
|
||||
// user
|
||||
repo := new(api.Repository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: user.Name,
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, repo)
|
||||
t.Run("permitted to generate into user with user scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
|
||||
repo := new(api.Repository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: user.Name,
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, repo)
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
})
|
||||
|
||||
// org
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: "org3",
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, repo)
|
||||
t.Run("denied to generate into user without user scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: user.Name,
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token requires scope write:user to create a repository owned by a user")
|
||||
})
|
||||
|
||||
t.Run("permitted to generate into org with org scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
|
||||
repo := new(api.Repository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: "org3",
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, repo)
|
||||
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
})
|
||||
|
||||
t.Run("denied to generate into org without org scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: "org3",
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token requires scope write:organization to create a repository owned by a user")
|
||||
})
|
||||
|
||||
t.Run("denied to generate without write:repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
Owner: user.Name,
|
||||
Name: "new-repo",
|
||||
Description: "test generate repo",
|
||||
Private: false,
|
||||
GitContent: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "token does not have at least one of required scope(s): [write:repository]")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoGetReviewers(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -346,16 +346,6 @@ func TestAPIDeniesPermissionBasedOnTokenScope(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"/api/v1/repos/user1/repo1",
|
||||
"DELETE",
|
||||
[]permission{
|
||||
{
|
||||
auth_model.AccessTokenScopeCategoryRepository,
|
||||
auth_model.Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"/api/v1/repos/user1/repo1/branches",
|
||||
"GET",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue