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:
Mathieu Fenniak 2026-03-20 16:14:36 +01:00 committed by Mathieu Fenniak
parent dc65408618
commit a27f9a719e
11 changed files with 264 additions and 53 deletions

1
release-notes/11736.md Normal file
View 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.

View file

@ -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())

View file

@ -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}))
}

View file

@ -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
}
}
}

View file

@ -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"})

View file

@ -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)
})
}

View file

@ -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)
})
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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",