2019-04-17 10:06:35 -06:00
|
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
2022-11-27 13:20:29 -05:00
|
|
|
// SPDX-License-Identifier: MIT
|
2019-04-17 10:06:35 -06:00
|
|
|
|
2022-09-02 15:18:23 -04:00
|
|
|
package integration
|
2019-04-17 10:06:35 -06:00
|
|
|
|
|
|
|
import (
|
2022-03-29 21:13:41 +02:00
|
|
|
stdCtx "context"
|
2019-04-17 10:06:35 -06:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2019-05-11 16:29:17 +01:00
|
|
|
"net/url"
|
2019-04-17 10:06:35 -06:00
|
|
|
"path/filepath"
|
|
|
|
"testing"
|
2019-12-24 03:33:52 +01:00
|
|
|
"time"
|
2019-04-17 10:06:35 -06:00
|
|
|
|
2023-01-17 16:46:03 -05:00
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
2021-12-10 09:27:50 +08:00
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2021-11-16 16:53:21 +08:00
|
|
|
"code.gitea.io/gitea/models/unittest"
|
2021-11-24 17:49:20 +08:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
|
|
|
"code.gitea.io/gitea/modules/gitrepo"
|
2019-04-17 10:06:35 -06:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2019-05-11 18:21:34 +08:00
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2024-02-27 15:12:22 +08:00
|
|
|
"code.gitea.io/gitea/services/context"
|
2019-04-17 10:06:35 -06:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
)
|
|
|
|
|
|
|
|
func getCreateFileOptions() api.CreateFileOptions {
|
|
|
|
content := "This is new text"
|
|
|
|
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
|
|
|
|
return api.CreateFileOptions{
|
|
|
|
FileOptions: api.FileOptions{
|
|
|
|
BranchName: "master",
|
|
|
|
NewBranchName: "master",
|
2019-06-29 11:19:24 -04:00
|
|
|
Message: "Making this new file new/file.txt",
|
2019-04-17 10:06:35 -06:00
|
|
|
Author: api.Identity{
|
2019-12-09 14:11:24 +01:00
|
|
|
Name: "Anne Doe",
|
|
|
|
Email: "annedoe@example.com",
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
|
|
|
Committer: api.Identity{
|
2019-12-09 14:11:24 +01:00
|
|
|
Name: "John Doe",
|
|
|
|
Email: "johndoe@example.com",
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
2019-12-24 03:33:52 +01:00
|
|
|
Dates: api.CommitDateOptions{
|
|
|
|
Author: time.Unix(946684810, 0),
|
|
|
|
Committer: time.Unix(978307190, 0),
|
|
|
|
},
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
2023-07-19 02:14:47 +08:00
|
|
|
ContentBase64: contentEncoded,
|
2019-04-17 10:06:35 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 10:09:04 +02:00
|
|
|
func getExpectedFileResponseForCreate(repoFullName, commitID, treePath, latestCommitSHA string) *api.FileResponse {
|
2019-04-17 10:06:35 -06:00
|
|
|
sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
|
2019-06-29 16:51:10 -04:00
|
|
|
encoding := "base64"
|
|
|
|
content := "VGhpcyBpcyBuZXcgdGV4dA=="
|
2022-03-28 20:48:41 +01:00
|
|
|
selfURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/contents/" + treePath + "?ref=master"
|
|
|
|
htmlURL := setting.AppURL + repoFullName + "/src/branch/master/" + treePath
|
|
|
|
gitURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/git/blobs/" + sha
|
|
|
|
downloadURL := setting.AppURL + repoFullName + "/raw/branch/master/" + treePath
|
2019-04-17 10:06:35 -06:00
|
|
|
return &api.FileResponse{
|
2019-06-29 16:51:10 -04:00
|
|
|
Content: &api.ContentsResponse{
|
2022-07-30 10:09:04 +02:00
|
|
|
Name: filepath.Base(treePath),
|
|
|
|
Path: treePath,
|
|
|
|
SHA: sha,
|
|
|
|
LastCommitSHA: latestCommitSHA,
|
|
|
|
Size: 16,
|
|
|
|
Type: "file",
|
|
|
|
Encoding: &encoding,
|
|
|
|
Content: &content,
|
|
|
|
URL: &selfURL,
|
|
|
|
HTMLURL: &htmlURL,
|
|
|
|
GitURL: &gitURL,
|
|
|
|
DownloadURL: &downloadURL,
|
2019-04-17 10:06:35 -06:00
|
|
|
Links: &api.FileLinksResponse{
|
2019-06-29 16:51:10 -04:00
|
|
|
Self: &selfURL,
|
|
|
|
GitURL: &gitURL,
|
|
|
|
HTMLURL: &htmlURL,
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Commit: &api.FileCommitResponse{
|
|
|
|
CommitMeta: api.CommitMeta{
|
2022-03-28 20:48:41 +01:00
|
|
|
URL: setting.AppURL + "api/v1/repos/" + repoFullName + "/git/commits/" + commitID,
|
2019-04-17 10:06:35 -06:00
|
|
|
SHA: commitID,
|
|
|
|
},
|
2022-03-28 20:48:41 +01:00
|
|
|
HTMLURL: setting.AppURL + repoFullName + "/commit/" + commitID,
|
2019-04-17 10:06:35 -06:00
|
|
|
Author: &api.CommitUser{
|
|
|
|
Identity: api.Identity{
|
2019-12-09 14:11:24 +01:00
|
|
|
Name: "Anne Doe",
|
|
|
|
Email: "annedoe@example.com",
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
2019-12-24 03:33:52 +01:00
|
|
|
Date: "2000-01-01T00:00:10Z",
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
|
|
|
Committer: &api.CommitUser{
|
|
|
|
Identity: api.Identity{
|
|
|
|
Name: "John Doe",
|
|
|
|
Email: "johndoe@example.com",
|
|
|
|
},
|
2019-12-24 03:33:52 +01:00
|
|
|
Date: "2000-12-31T23:59:50Z",
|
2019-04-17 10:06:35 -06:00
|
|
|
},
|
|
|
|
Message: "Updates README.md\n",
|
|
|
|
},
|
|
|
|
Verification: &api.PayloadCommitVerification{
|
|
|
|
Verified: false,
|
2019-10-16 14:42:42 +01:00
|
|
|
Reason: "gpg.error.not_signed_commit",
|
2019-04-17 10:06:35 -06:00
|
|
|
Signature: "",
|
|
|
|
Payload: "",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-16 20:30:16 +02:00
|
|
|
func BenchmarkAPICreateFileSmall(b *testing.B) {
|
2023-08-13 00:30:16 +08:00
|
|
|
onGiteaRun(b, func(b *testing.B, u *url.URL) {
|
|
|
|
user2 := unittest.AssertExistsAndLoadBean(b, &user_model.User{ID: 2}) // owner of the repo1 & repo16
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo
|
2021-04-16 20:30:16 +02:00
|
|
|
|
2023-08-13 00:30:16 +08:00
|
|
|
b.ResetTimer()
|
2021-04-16 20:30:16 +02:00
|
|
|
for n := 0; n < b.N; n++ {
|
|
|
|
treePath := fmt.Sprintf("update/file%d.txt", n)
|
2023-08-13 00:30:16 +08:00
|
|
|
_, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
|
2021-04-16 20:30:16 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func BenchmarkAPICreateFileMedium(b *testing.B) {
|
|
|
|
data := make([]byte, 10*1024*1024)
|
|
|
|
|
2023-08-13 00:30:16 +08:00
|
|
|
onGiteaRun(b, func(b *testing.B, u *url.URL) {
|
|
|
|
user2 := unittest.AssertExistsAndLoadBean(b, &user_model.User{ID: 2}) // owner of the repo1 & repo16
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo
|
2021-04-16 20:30:16 +02:00
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
|
|
treePath := fmt.Sprintf("update/file%d.txt", n)
|
|
|
|
copy(data, treePath)
|
2023-08-13 00:30:16 +08:00
|
|
|
_, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
|
2021-04-16 20:30:16 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-17 10:06:35 -06:00
|
|
|
func TestAPICreateFile(t *testing.T) {
|
2019-05-11 16:29:17 +01:00
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
2022-08-16 10:22:25 +08:00
|
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
|
2023-09-14 10:59:53 +08:00
|
|
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
|
2022-08-16 10:22:25 +08:00
|
|
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
|
|
|
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
|
|
|
|
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
|
2019-05-11 16:29:17 +01:00
|
|
|
fileID := 0
|
|
|
|
|
|
|
|
// Get user2's token
|
|
|
|
session := loginUser(t, user2.Name)
|
Redesign Scoped Access Tokens (#24767)
## Changes
- Adds the following high level access scopes, each with `read` and
`write` levels:
- `activitypub`
- `admin` (hidden if user is not a site admin)
- `misc`
- `notification`
- `organization`
- `package`
- `issue`
- `repository`
- `user`
- Adds new middleware function `tokenRequiresScopes()` in addition to
`reqToken()`
- `tokenRequiresScopes()` is used for each high-level api section
- _if_ a scoped token is present, checks that the required scope is
included based on the section and HTTP method
- `reqToken()` is used for individual routes
- checks that required authentication is present (but does not check
scope levels as this will already have been handled by
`tokenRequiresScopes()`
- Adds migration to convert old scoped access tokens to the new set of
scopes
- Updates the user interface for scope selection
### User interface example
<img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3">
<img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c">
## tokenRequiresScopes Design Decision
- `tokenRequiresScopes()` was added to more reliably cover api routes.
For an incoming request, this function uses the given scope category
(say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say
`DELETE`) and verifies that any scoped tokens in use include
`delete:organization`.
- `reqToken()` is used to enforce auth for individual routes that
require it. If a scoped token is not present for a request,
`tokenRequiresScopes()` will not return an error
## TODO
- [x] Alphabetize scope categories
- [x] Change 'public repos only' to a radio button (private vs public).
Also expand this to organizations
- [X] Disable token creation if no scopes selected. Alternatively, show
warning
- [x] `reqToken()` is missing from many `POST/DELETE` routes in the api.
`tokenRequiresScopes()` only checks that a given token has the correct
scope, `reqToken()` must be used to check that a token (or some other
auth) is present.
- _This should be addressed in this PR_
- [x] The migration should be reviewed very carefully in order to
minimize access changes to existing user tokens.
- _This should be addressed in this PR_
- [x] Link to api to swagger documentation, clarify what
read/write/delete levels correspond to
- [x] Review cases where more than one scope is needed as this directly
deviates from the api definition.
- _This should be addressed in this PR_
- For example:
```go
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization),
context_service.UserAssignmentAPI())
```
## Future improvements
- [ ] Add required scopes to swagger documentation
- [ ] Redesign `reqToken()` to be opt-out rather than opt-in
- [ ] Subdivide scopes like `repository`
- [ ] Once a token is created, if it has no scopes, we should display
text instead of an empty bullet point
- [ ] If the 'public repos only' option is selected, should read
categories be selected by default
Closes #24501
Closes #24799
Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2023-06-04 14:57:16 -04:00
|
|
|
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
2019-05-11 16:29:17 +01:00
|
|
|
// Get user4's token
|
|
|
|
session = loginUser(t, user4.Name)
|
Redesign Scoped Access Tokens (#24767)
## Changes
- Adds the following high level access scopes, each with `read` and
`write` levels:
- `activitypub`
- `admin` (hidden if user is not a site admin)
- `misc`
- `notification`
- `organization`
- `package`
- `issue`
- `repository`
- `user`
- Adds new middleware function `tokenRequiresScopes()` in addition to
`reqToken()`
- `tokenRequiresScopes()` is used for each high-level api section
- _if_ a scoped token is present, checks that the required scope is
included based on the section and HTTP method
- `reqToken()` is used for individual routes
- checks that required authentication is present (but does not check
scope levels as this will already have been handled by
`tokenRequiresScopes()`
- Adds migration to convert old scoped access tokens to the new set of
scopes
- Updates the user interface for scope selection
### User interface example
<img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3">
<img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c">
## tokenRequiresScopes Design Decision
- `tokenRequiresScopes()` was added to more reliably cover api routes.
For an incoming request, this function uses the given scope category
(say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say
`DELETE`) and verifies that any scoped tokens in use include
`delete:organization`.
- `reqToken()` is used to enforce auth for individual routes that
require it. If a scoped token is not present for a request,
`tokenRequiresScopes()` will not return an error
## TODO
- [x] Alphabetize scope categories
- [x] Change 'public repos only' to a radio button (private vs public).
Also expand this to organizations
- [X] Disable token creation if no scopes selected. Alternatively, show
warning
- [x] `reqToken()` is missing from many `POST/DELETE` routes in the api.
`tokenRequiresScopes()` only checks that a given token has the correct
scope, `reqToken()` must be used to check that a token (or some other
auth) is present.
- _This should be addressed in this PR_
- [x] The migration should be reviewed very carefully in order to
minimize access changes to existing user tokens.
- _This should be addressed in this PR_
- [x] Link to api to swagger documentation, clarify what
read/write/delete levels correspond to
- [x] Review cases where more than one scope is needed as this directly
deviates from the api definition.
- _This should be addressed in this PR_
- For example:
```go
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization),
context_service.UserAssignmentAPI())
```
## Future improvements
- [ ] Add required scopes to swagger documentation
- [ ] Redesign `reqToken()` to be opt-out rather than opt-in
- [ ] Subdivide scopes like `repository`
- [ ] Once a token is created, if it has no scopes, we should display
text instead of an empty bullet point
- [ ] If the 'public repos only' option is selected, should read
categories be selected by default
Closes #24501
Closes #24799
Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2023-06-04 14:57:16 -04:00
|
|
|
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
|
|
|
// Test creating a file in repo1 which user2 owns, try both with branch and empty branch
|
|
|
|
for _, branch := range [...]string{
|
|
|
|
"master", // Branch
|
|
|
|
"", // Empty branch
|
|
|
|
} {
|
|
|
|
createFileOptions := getCreateFileOptions()
|
|
|
|
createFileOptions.BranchName = branch
|
|
|
|
fileID++
|
|
|
|
treePath := fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
|
|
|
gitRepo, _ := gitrepo.OpenRepository(stdCtx.Background(), repo1)
|
2019-05-11 16:29:17 +01:00
|
|
|
commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
|
2022-07-30 10:09:04 +02:00
|
|
|
latestCommit, _ := gitRepo.GetCommitByPath(treePath)
|
|
|
|
expectedFileResponse := getExpectedFileResponseForCreate("user2/repo1", commitID, treePath, latestCommit.ID.String())
|
2019-05-11 16:29:17 +01:00
|
|
|
var fileResponse api.FileResponse
|
|
|
|
DecodeJSON(t, resp, &fileResponse)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
|
2019-12-24 03:33:52 +01:00
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
|
2019-11-13 07:01:19 +00:00
|
|
|
gitRepo.Close()
|
2019-05-11 16:29:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test creating a file in a new branch
|
2019-04-17 10:06:35 -06:00
|
|
|
createFileOptions := getCreateFileOptions()
|
2019-05-11 16:29:17 +01:00
|
|
|
createFileOptions.BranchName = repo1.DefaultBranch
|
|
|
|
createFileOptions.NewBranchName = "new_branch"
|
2019-04-17 10:06:35 -06:00
|
|
|
fileID++
|
|
|
|
treePath := fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
2019-04-17 10:06:35 -06:00
|
|
|
var fileResponse api.FileResponse
|
|
|
|
DecodeJSON(t, resp, &fileResponse)
|
2019-05-11 16:29:17 +01:00
|
|
|
expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
|
2019-06-29 16:51:10 -04:00
|
|
|
expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
|
2019-06-08 10:31:11 -04:00
|
|
|
expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
|
2019-05-11 16:29:17 +01:00
|
|
|
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
|
2019-06-29 16:51:10 -04:00
|
|
|
assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
|
|
|
|
assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
|
2019-06-29 11:19:24 -04:00
|
|
|
assert.EqualValues(t, createFileOptions.Message+"\n", fileResponse.Commit.Message)
|
|
|
|
|
|
|
|
// Test creating a file without a message
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
createFileOptions.Message = ""
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
resp = MakeRequest(t, req, http.StatusCreated)
|
2019-06-29 11:19:24 -04:00
|
|
|
DecodeJSON(t, resp, &fileResponse)
|
2023-04-17 22:04:26 +00:00
|
|
|
expectedMessage := "Add " + treePath + "\n"
|
2019-06-29 11:19:24 -04:00
|
|
|
assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
|
|
|
// Test trying to create a file that already exists, should fail
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
treePath = "README.md"
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
|
2019-05-11 16:29:17 +01:00
|
|
|
expectedAPIError := context.APIError{
|
|
|
|
Message: "repository file already exists [path: " + treePath + "]",
|
2019-06-12 16:07:24 -05:00
|
|
|
URL: setting.API.SwaggerURL,
|
2019-05-11 16:29:17 +01:00
|
|
|
}
|
|
|
|
var apiError context.APIError
|
|
|
|
DecodeJSON(t, resp, &apiError)
|
|
|
|
assert.Equal(t, expectedAPIError, apiError)
|
|
|
|
|
|
|
|
// Test creating a file in repo1 by user4 who does not have write access
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token4)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusNotFound)
|
2019-04-17 10:06:35 -06:00
|
|
|
|
2019-05-11 16:29:17 +01:00
|
|
|
// Tests a repo with no token given so will fail
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusNotFound)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
|
|
|
// Test using access token for a private repo that the user of the token owns
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusCreated)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
2023-09-14 10:59:53 +08:00
|
|
|
// Test using org repo "org3/repo3" where user2 is a collaborator
|
2019-05-11 16:29:17 +01:00
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusCreated)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
2023-09-14 10:59:53 +08:00
|
|
|
// Test using org repo "org3/repo3" with no user token
|
2019-05-11 16:29:17 +01:00
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &createFileOptions)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusNotFound)
|
2019-05-11 16:29:17 +01:00
|
|
|
|
|
|
|
// Test using repo "user2/repo1" where user4 is a NOT collaborator
|
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token4)
|
2022-12-02 11:39:42 +08:00
|
|
|
MakeRequest(t, req, http.StatusForbidden)
|
2022-03-28 20:48:41 +01:00
|
|
|
|
|
|
|
// Test creating a file in an empty repository
|
Redesign Scoped Access Tokens (#24767)
## Changes
- Adds the following high level access scopes, each with `read` and
`write` levels:
- `activitypub`
- `admin` (hidden if user is not a site admin)
- `misc`
- `notification`
- `organization`
- `package`
- `issue`
- `repository`
- `user`
- Adds new middleware function `tokenRequiresScopes()` in addition to
`reqToken()`
- `tokenRequiresScopes()` is used for each high-level api section
- _if_ a scoped token is present, checks that the required scope is
included based on the section and HTTP method
- `reqToken()` is used for individual routes
- checks that required authentication is present (but does not check
scope levels as this will already have been handled by
`tokenRequiresScopes()`
- Adds migration to convert old scoped access tokens to the new set of
scopes
- Updates the user interface for scope selection
### User interface example
<img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3">
<img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c">
## tokenRequiresScopes Design Decision
- `tokenRequiresScopes()` was added to more reliably cover api routes.
For an incoming request, this function uses the given scope category
(say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say
`DELETE`) and verifies that any scoped tokens in use include
`delete:organization`.
- `reqToken()` is used to enforce auth for individual routes that
require it. If a scoped token is not present for a request,
`tokenRequiresScopes()` will not return an error
## TODO
- [x] Alphabetize scope categories
- [x] Change 'public repos only' to a radio button (private vs public).
Also expand this to organizations
- [X] Disable token creation if no scopes selected. Alternatively, show
warning
- [x] `reqToken()` is missing from many `POST/DELETE` routes in the api.
`tokenRequiresScopes()` only checks that a given token has the correct
scope, `reqToken()` must be used to check that a token (or some other
auth) is present.
- _This should be addressed in this PR_
- [x] The migration should be reviewed very carefully in order to
minimize access changes to existing user tokens.
- _This should be addressed in this PR_
- [x] Link to api to swagger documentation, clarify what
read/write/delete levels correspond to
- [x] Review cases where more than one scope is needed as this directly
deviates from the api definition.
- _This should be addressed in this PR_
- For example:
```go
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization),
context_service.UserAssignmentAPI())
```
## Future improvements
- [ ] Add required scopes to swagger documentation
- [ ] Redesign `reqToken()` to be opt-out rather than opt-in
- [ ] Subdivide scopes like `repository`
- [ ] Once a token is created, if it has no scopes, we should display
text instead of an empty bullet point
- [ ] If the 'public repos only' option is selected, should read
categories be selected by default
Closes #24501
Closes #24799
Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2023-06-04 14:57:16 -04:00
|
|
|
doAPICreateRepository(NewAPITestContext(t, "user2", "empty-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser), true)(t)
|
2022-03-28 20:48:41 +01:00
|
|
|
createFileOptions = getCreateFileOptions()
|
|
|
|
fileID++
|
|
|
|
treePath = fmt.Sprintf("new/file%d.txt", fileID)
|
2023-12-22 00:59:59 +01:00
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, "empty-repo", treePath), &createFileOptions).
|
|
|
|
AddTokenAuth(token2)
|
2022-12-02 11:39:42 +08:00
|
|
|
resp = MakeRequest(t, req, http.StatusCreated)
|
2022-08-16 10:22:25 +08:00
|
|
|
emptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "empty-repo"}) // public repo
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
|
|
|
gitRepo, _ := gitrepo.OpenRepository(stdCtx.Background(), emptyRepo)
|
2022-03-28 20:48:41 +01:00
|
|
|
commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
|
2022-07-30 10:09:04 +02:00
|
|
|
latestCommit, _ := gitRepo.GetCommitByPath(treePath)
|
|
|
|
expectedFileResponse := getExpectedFileResponseForCreate("user2/empty-repo", commitID, treePath, latestCommit.ID.String())
|
2022-03-28 20:48:41 +01:00
|
|
|
DecodeJSON(t, resp, &fileResponse)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
|
|
|
|
assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
|
|
|
|
gitRepo.Close()
|
2019-05-11 16:29:17 +01:00
|
|
|
})
|
2019-04-17 10:06:35 -06:00
|
|
|
}
|