Merge branch 'main' into add-repo-license-display

This commit is contained in:
yp05327 2023-06-05 17:18:59 +09:00 committed by GitHub
commit a000c38ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
187 changed files with 3059 additions and 1787 deletions

View File

@ -76,7 +76,6 @@ cpu.out
/yarn-error.log
/npm-debug.log*
/public/js
/public/serviceworker.js
/public/css
/public/fonts
/public/img/webpack

2
.gitignore vendored
View File

@ -47,6 +47,7 @@ cpu.out
*.db
*.log
*.log.*.gz
/gitea
/gitea-vet
@ -77,7 +78,6 @@ cpu.out
/yarn-error.log
/npm-debug.log*
/public/js
/public/serviceworker.js
/public/css
/public/fonts
/public/img/webpack

View File

@ -86,6 +86,7 @@ linters-settings:
- io/ioutil: "use os or io instead"
- golang.org/x/exp: "it's experimental and unreliable."
- code.gitea.io/gitea/modules/git/internal: "do not use the internal package, use AddXxx function instead"
- gopkg.in/ini.v1: "do not use the ini package, use gitea's config system instead"
issues:
max-issues-per-linter: 0

View File

@ -1,5 +1,6 @@
*.min.css
*.min.js
/assets/*.json
/modules/options/bindata.go
/modules/public/bindata.go
/modules/templates/bindata.go

View File

@ -114,7 +114,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
WEBPACK_CONFIGS := webpack.config.js
WEBPACK_DEST := public/js/index.js public/css/index.css
WEBPACK_DEST_ENTRIES := public/js public/css public/fonts public/img/webpack public/serviceworker.js
WEBPACK_DEST_ENTRIES := public/js public/css public/fonts public/img/webpack
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))

View File

@ -12,7 +12,7 @@ import (
"path/filepath"
"strings"
"gopkg.in/ini.v1"
"code.gitea.io/gitea/modules/setting"
)
func main() {
@ -22,14 +22,13 @@ func main() {
os.Exit(1)
}
ini.PrettyFormat = false
mustNoErr := func(err error) {
if err != nil {
panic(err)
}
}
collectInis := func(ref string) map[string]*ini.File {
inis := map[string]*ini.File{}
collectInis := func(ref string) map[string]setting.ConfigProvider {
inis := map[string]setting.ConfigProvider{}
err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
@ -37,10 +36,7 @@ func main() {
if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") {
return nil
}
cfg, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, path)
cfg, err := setting.NewConfigProviderForLocale(path)
mustNoErr(err)
inis[path] = cfg
fmt.Printf("collecting: %s @ %s\n", path, ref)

View File

@ -9,10 +9,8 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/urfave/cli"
"gopkg.in/ini.v1"
)
// EnvironmentPrefix environment variables prefixed with this represent ini values to write
@ -97,19 +95,10 @@ func runEnvironmentToIni(c *cli.Context) error {
providedWorkPath := c.String("work-path")
setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
cfg := ini.Empty()
confFileExists, err := util.IsFile(setting.CustomConf)
cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true})
if err != nil {
log.Fatal("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
if confFileExists {
if err := cfg.Append(setting.CustomConf); err != nil {
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
} else {
log.Warn("Custom config '%s' not found, ignore this if you're running first time", setting.CustomConf)
}
cfg.NameMapper = ini.SnackCase
prefixGitea := c.String("prefix") + "__"
suffixFile := "__FILE"

View File

@ -1208,9 +1208,6 @@ LEVEL = Info
;; Whether to search within description at repository search on explore page.
;SEARCH_REPO_DESCRIPTION = true
;;
;; Whether to enable a Service Worker to cache frontend assets
;USE_SERVICE_WORKER = false
;;
;; Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used.
;; A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic).
;ONLY_SHOW_RELEVANT_REPOS = false

View File

@ -230,7 +230,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a
add it to this config.
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **false**: Whether to enable a Service Worker to cache frontend assets.
- `ONLY_SHOW_RELEVANT_REPOS`: **false** Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used.
A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic).

View File

@ -1,5 +1,5 @@
---
date: "2019-04-19:44:00+01:00"
date: "2023-06-01T08:40:00+08:00"
title: "OAuth2 provider"
slug: "oauth2-provider"
weight: 41
@ -40,46 +40,47 @@ At the moment Gitea only supports the [**Authorization Code Grant**](https://too
- [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636)
- [OpenID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
To use the Authorization Code Grant as a third party application it is required to register a new application via the "Settings" (`/user/settings/applications`) section of the settings.
To use the Authorization Code Grant as a third party application it is required to register a new application via the "Settings" (`/user/settings/applications`) section of the settings. To test or debug you can use the web-tool https://oauthdebugger.com/.
## Scopes
Gitea supports the following scopes for tokens:
Gitea supports scoped access tokens, which allow users the ability to restrict tokens to operate only on selected url routes. Scopes are grouped by high-level API routes, and further refined to the following:
| Name | Description |
| ---- | ----------- |
| **(no scope)** | Grants read-only access to public user profile and public repositories. |
| **repo** | Full control over all repositories. |
|     **repo:status** | Grants read/write access to commit status in all repositories. |
|     **public_repo** | Grants read/write access to public repositories only. |
| **admin:repo_hook** | Grants access to repository hooks of all repositories. This is included in the `repo` scope. |
|     **write:repo_hook** | Grants read/write access to repository hooks |
|     **read:repo_hook** | Grants read-only access to repository hooks |
| **admin:org** | Grants full access to organization settings |
|     **write:org** | Grants read/write access to organization settings |
|     **read:org** | Grants read-only access to organization settings |
| **admin:public_key** | Grants full access for managing public keys |
|     **write:public_key** | Grant read/write access to public keys |
|     **read:public_key** | Grant read-only access to public keys |
| **admin:org_hook** | Grants full access to organizational-level hooks |
| **admin:user_hook** | Grants full access to user-level hooks |
| **notification** | Grants full access to notifications |
| **user** | Grants full access to user profile info |
|     **read:user** | Grants read access to user's profile |
|     **user:email** | Grants read access to user's email addresses |
|     **user:follow** | Grants access to follow/un-follow a user |
| **delete_repo** | Grants access to delete repositories as an admin |
| **package** | Grants full access to hosted packages |
|     **write:package** | Grants read/write access to packages |
|     **read:package** | Grants read access to packages |
|     **delete:package** | Grants delete access to packages |
| **admin:gpg_key** | Grants full access for managing GPG keys |
|     **write:gpg_key** | Grants read/write access to GPG keys |
|     **read:gpg_key** | Grants read-only access to GPG keys |
| **admin:application** | Grants full access to manage applications |
|     **write:application** | Grants read/write access for managing applications |
|     **read:application** | Grants read access for managing applications |
| **sudo** | Allows to perform actions as the site admin. |
- `read`: `GET` routes
- `write`: `POST`, `PUT`, `PATCH`, and `DELETE` routes (in addition to `GET`)
Gitea token scopes are as follows:
| Name | Description |
| ---- |--------------------------------------------------------------------------------------------------------------------------------------------------|
| **(no scope)** | Not supported. A scope is required even for public repositories. |
| **activitypub** | `activitypub` API routes: ActivityPub related operations. |
|     **read:activitypub** | Grants read access for ActivityPub operations. |
|     **write:activitypub** | Grants read/write/delete access for ActivityPub operations. |
| **admin** | `/admin/*` API routes: Site-wide administrative operations (hidden for non-admin accounts). |
|     **read:admin** | Grants read access for admin operations, such as getting cron jobs or registered user emails. |
|     **write:admin** | Grants read/write/delete access for admin operations, such as running cron jobs or updating user accounts. | |
| **issue** | `issues/*`, `labels/*`, `milestones/*` API routes: Issue-related operations. |
|     **read:issue** | Grants read access for issues operations, such as getting issue comments, issue attachments, and milestones. |
|     **write:issue** | Grants read/write/delete access for issues operations, such as posting or editing an issue comment or attachment, and updating milestones. |
| **misc** | miscellaneous and settings top-level API routes. |
|     **read:misc** | Grants read access to miscellaneous operations, such as getting label and gitignore templates. |
|     **write:misc** | Grants read/write/delete access to miscellaneous operations, such as markup utility operations. |
| **notification** | `notification/*` API routes: user notification operations. |
|     **read:notification** | Grants read access to user notifications, such as which notifications users are subscribed to and read new notifications. |
|     **write:notification** | Grants read/write/delete access to user notifications, such as marking notifications as read. |
| **organization** | `orgs/*` and `teams/*` API routes: Organization and team management operations. |
|     **read:organization** | Grants read access to org and team status, such as listing all orgs a user has visibility to, teams, and team members. |
|     **write:organization** | Grants read/write/delete access to org and team status, such as creating and updating teams and updating org settings. |
| **package** | `/packages/*` API routes: Packages operations |
|     **read:package** | Grants read access to package operations, such as reading and downloading available packages. |
|     **write:package** | Grants read/write/delete access to package operations. Currently the same as `read:package`. |
| **repository** | `/repos/*` API routes except `/repos/issues/*`: Repository file, pull-request, and release operations. |
|     **read:repository** | Grants read access to repository operations, such as getting repository files, releases, collaborators. |
|     **write:repository** | Grants read/write/delete access to repository operations, such as getting updating repository files, creating pull requests, updating collaborators. |
| **user** | `/user/*` and `/users/*` API routes: User-related operations. |
|     **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. |
|     **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings. |
## Client types
@ -87,17 +88,19 @@ Gitea supports both confidential and public client types, [as defined by RFC 674
For public clients, a redirect URI of a loopback IP address such as `http://127.0.0.1/` allows any port. Avoid using `localhost`, [as recommended by RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3).
## Example
## Examples
### Confidential client
**Note:** This example does not use PKCE.
1. Redirect to user to the authorization endpoint in order to get their consent for accessing the resources:
1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources:
```curl
https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE
```
The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be send back to your application after the user authorizes. The `state` parameter is optional but should be used to prevent CSRF attacks.
The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be sent back to your application after the user authorizes. The `state` parameter is optional, but should be used to prevent CSRF attacks.
![Authorization Page](/authorize.png)
@ -107,7 +110,7 @@ For public clients, a redirect URI of a loopback IP address such as `http://127.
https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE
```
2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoints accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example:
2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example:
```curl
POST https://[YOUR-GITEA-URL]/login/oauth/access_token
@ -134,7 +137,69 @@ For public clients, a redirect URI of a loopback IP address such as `http://127.
}
```
The `CLIENT_SECRET` is the unique secret code generated for this application. Please note that the secret will only be visible after you created/registered the application with Gitea and cannot be recovered. If you lose the secret you must regenerate the secret via the application's settings.
The `CLIENT_SECRET` is the unique secret code generated for this application. Please note that the secret will only be visible after you created/registered the application with Gitea and cannot be recovered. If you lose the secret, you must regenerate the secret via the application's settings.
The `REDIRECT_URI` in the `access_token` request must match the `REDIRECT_URI` in the `authorize` request.
3. Use the `access_token` to make [API requests](https://docs.gitea.io/en-us/api-usage#oauth2) to access the user's resources.
### Public client (PKCE)
PKCE (Proof Key for Code Exchange) is an extension to the OAuth flow which allows for a secure credential exchange without the requirement to provide a client secret.
**Note**: Please ensure you have registered your OAuth application as a public client.
To achieve this, you have to provide a `code_verifier` for every authorization request. A `code_verifier` has to be a random string with a minimum length of 43 characters and a maximum length of 128 characters. It can contain alphanumeric characters as well as the characters `-`, `.`, `_` and `~`.
Using this `code_verifier` string, a new one called `code_challenge` is created by using one of two methods:
- If you have the required functionality on your client, set `code_challenge` to be a URL-safe base64-encoded string of the SHA256 hash of `code_verifier`. In that case, your `code_challenge_method` becomes `S256`.
- If you are unable to do so, you can provide your `code_verifier` as a plain string to `code_challenge`. Then you have to set your `code_challenge_method` as `plain`.
After you have generated this values, you can continue with your request.
1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources:
```curl
https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&code_challenge_method=CODE_CHALLENGE_METHOD&code_challenge=CODE_CHALLENGE&state=STATE
```
The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be sent back to your application after the user authorizes. The `state` parameter is optional, but should be used to prevent CSRF attacks.
![Authorization Page](/authorize.png)
The user will now be asked to authorize your application. If they authorize it, the user will be redirected to the `REDIRECT_URL`, for example:
```curl
https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE
```
2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example:
```curl
POST https://[YOUR-GITEA-URL]/login/oauth/access_token
```
```json
{
"client_id": "YOUR_CLIENT_ID",
"code": "RETURNED_CODE",
"grant_type": "authorization_code",
"redirect_uri": "REDIRECT_URI",
"code_verifier": "CODE_VERIFIER",
}
```
Response:
```json
{
"access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJnbnQiOjIsInR0IjowLCJleHAiOjE1NTUxNzk5MTIsImlhdCI6MTU1NTE3NjMxMn0.0-iFsAwBtxuckA0sNZ6QpBQmywVPz129u75vOM7wPJecw5wqGyBkmstfJHAjEOqrAf_V5Z-1QYeCh_Cz4RiKug",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJnbnQiOjIsInR0IjoxLCJjbnQiOjEsImV4cCI6MTU1NzgwNDMxMiwiaWF0IjoxNTU1MTc2MzEyfQ.S_HZQBy4q9r5SEzNGNIoFClT43HPNDbUdHH-GYNYYdkRfft6XptJBkUQscZsGxOW975Yk6RbgtGvq1nkEcklOw"
}
```
The `REDIRECT_URI` in the `access_token` request must match the `REDIRECT_URI` in the `authorize` request.

View File

@ -27,7 +27,7 @@ For organizations, you can define organization-wide labels that are shared with
Labels have a mandatory name, a mandatory color, an optional description, and must either be exclusive or not (see `Scoped Labels` below).
When you create a repository, you can ensure certain labels exist by using the `Issue Labels` option. This option lists a number of available label sets that are [configured globally on your instance](../customizing-gitea/#labels). Its contained labels will all be created as well while creating the repository.
When you create a repository, you can ensure certain labels exist by using the `Issue Labels` option. This option lists a number of available label sets that are [configured globally on your instance](../administration/customizing-gitea/#labels). Its contained labels will all be created as well while creating the repository.
## Scoped Labels

View File

@ -27,7 +27,7 @@ menu:
标签具有必填的名称和颜色,可选的描述,以及必须是独占的或非独占的(见下面的“作用域标签”)。
当您创建一个仓库时,可以通过使用 `工单标签Issue Labels` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。
当您创建一个仓库时,可以通过使用 `工单标签Issue Labels` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../administration/customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。
## 作用域标签

View File

@ -0,0 +1,93 @@
---
date: "2023-01-01T00:00:00+00:00"
title: "CRAN 软件包注册表"
slug: "cran"
draft: false
toc: false
menu:
sidebar:
parent: "packages"
name: "CRAN"
weight: 35
identifier: "cran"
---
# CRAN 软件包注册表
将 [R](https://www.r-project.org/) 软件包发布到您的用户或组织的类似 [CRAN](https://cran.r-project.org/) 的注册表。
**目录**
{{< toc >}}
## 要求
要使用CRAN软件包注册表您需要安装 [R](https://cran.r-project.org/)。
## 配置软件包注册表
要注册软件包注册表,您需要将其添加到 `Rprofile.site` 文件中,可以是系统级别、用户级别 `~/.Rprofile` 或项目级别:
```
options("repos" = c(getOption("repos"), c(gitea="https://gitea.example.com/api/packages/{owner}/cran")))
```
| 参数 | 描述 |
| ------- | -------------- |
| `owner` | 软件包的所有者 |
如果需要提供凭据可以将它们嵌入到URL(`https://user:password@gitea.example.com/...`)中。
## 发布软件包
要发布 R 软件包,请执行带有软件包内容的 HTTP `PUT` 操作。
源代码软件包:
```
PUT https://gitea.example.com/api/packages/{owner}/cran/src
```
| 参数 | 描述 |
| ------- | -------------- |
| `owner` | 软件包的所有者 |
二进制软件包:
```
PUT https://gitea.example.com/api/packages/{owner}/cran/bin?platform={platform}&rversion={rversion}
```
| 参数 | 描述 |
| ---------- | -------------- |
| `owner` | 软件包的所有者 |
| `platform` | 平台的名称 |
| `rversion` | 二进制的R版本 |
例如:
```shell
curl --user your_username:your_password_or_token \
--upload-file path/to/package.zip \
https://gitea.example.com/api/packages/testuser/cran/bin?platform=windows&rversion=4.2
```
如果同名和版本的软件包已存在,则无法发布软件包。您必须首先删除现有的软件包。
## 安装软件包
要从软件包注册表中安装R软件包请执行以下命令
```shell
install.packages("{package_name}")
```
| 参数 | 描述 |
| -------------- | ----------------- |
| `package_name` | The package name. |
例如:
```shell
install.packages("testpackage")
```

View File

@ -25,7 +25,7 @@ Gitea supports permissions for repository so that you can give different access
## Unit
In Gitea, we call a sub module of a repository `Unit`. Now we have following units.
In Gitea, we call a sub module of a repository `Unit`. Now we have following possible units.
| Name | Description | Permissions |
| --------------- | ---------------------------------------------------- | ----------- |
@ -37,6 +37,8 @@ In Gitea, we call a sub module of a repository `Unit`. Now we have following uni
| ExternalWiki | Link to an external wiki | Read |
| ExternalTracker | Link to an external issue tracker | Read |
| Projects | The URL to the template repository | Read Write |
| Packages | Packages which linked to this repository | Read Write |
| Actions | Review actions logs or restart/cacnel pipelines | Read Write |
| Settings | Manage the repository | Admin |
With different permissions, people could do different things with these units.
@ -51,6 +53,8 @@ With different permissions, people could do different things with these units.
| ExternalWiki | Link to an external wiki | - | - |
| ExternalTracker | Link to an external issue tracker | - | - |
| Projects | View the boards | Change issues across boards | - |
| Packages | View the packages | Upload/Delete packages | - |
| Actions | View the Actions logs | Approve / Cancel / Restart | - |
| Settings | - | - | Manage the repository |
And there are some differences for permissions between individual repositories and organization repositories.
@ -60,16 +64,27 @@ And there are some differences for permissions between individual repositories a
For individual repositories, the creators are the only owners of repositories and have no limit to change anything of this
repository or delete it. Repositories owners could add collaborators to help maintain the repositories. Collaborators could have `Read`, `Write` and `Admin` permissions.
For a private repository, the experience is similar to visiting an anonymous public repository. You have access to all the available content within the repository, including the ability to clone the code, create issues, respond to issue comments, submit pull requests, and more. If you have 'Write' permission, you can push code to specific branches of the repository, provided it's permitted by the branch protection rules. Additionally, you can make changes to the wiki pages. With 'Admin' permission, you have the ability to modify the repository's settings.
But you cannot delete or transfer this repository if you are not that repository's owner.
## Organization Repository
Different from individual repositories, the owner of organization repositories are the owner team of this organization.
For individual repositories, the owner is the user who created it. For organization repositories, the owners are the members of the owner team on this organization. All the permissions depends on the team permission settings.
### Team
### Owner Team
A team in an organization has unit permissions settings. It can have members and repositories scope. A team could access all the repositories in this organization or special repositories changed by the owner team. A team could also be allowed to create new
repositories.
The owner team will be created when the organization is created, and the creator will become the first member of the owner team. The owner team cannot be deleted and there is at least one member.
The owner team will be created when the organization is created, and the creator will become the first member of the owner team.
Every member of an organization must be in at least one team. The owner team cannot be deleted and only
members of the owner team can create a new team. An admin team can be created to manage some of the repositories, whose members can do anything with these repositories.
The Generate team can be created by the owner team to do the operations allowed by their permissions.
### Admin Team
When creating teams, there are two types of teams. One is the admin team, another is the general team. An admin team can be created to manage some of the repositories, whose members can do anything with these repositories. Only members of the owner or admin team can create a new team.
### General Team
A general team in an organization has unit permissions settings. It can have members and repositories scope.
- A team could access all the repositories in this organization or special repositories.
- A team could also be allowed to create new repositories or not.
The General team can be created to do the operations allowed by their permissions. One member could join multiple teams.

View File

@ -21,7 +21,7 @@ import (
type Statistic struct {
Counter struct {
User, Org, PublicKey,
Repo, Watch, Star, Action, Access,
Repo, Watch, Star, Access,
Issue, IssueClosed, IssueOpen,
Comment, Oauth, Follow,
Mirror, Release, AuthSource, Webhook,
@ -55,7 +55,6 @@ func GetStatistic() (stats Statistic) {
stats.Counter.Repo, _ = repo_model.CountRepositories(db.DefaultContext, repo_model.CountRepositoryOptions{})
stats.Counter.Watch, _ = e.Count(new(repo_model.Watch))
stats.Counter.Star, _ = e.Count(new(repo_model.Star))
stats.Counter.Action, _ = db.EstimateCount(db.DefaultContext, new(Action))
stats.Counter.Access, _ = e.Count(new(access_model.Access))
type IssueCount struct {
@ -83,7 +82,7 @@ func GetStatistic() (stats Statistic) {
Find(&stats.Counter.IssueByRepository)
}
issueCounts := []IssueCount{}
var issueCounts []IssueCount
_ = e.Select("COUNT(*) AS count, is_closed").Table("issue").GroupBy("is_closed").Find(&issueCounts)
for _, c := range issueCounts {

View File

@ -51,14 +51,6 @@ func (app *OAuth2Application) TableName() string {
return "oauth2_application"
}
// PrimaryRedirectURI returns the first redirect uri or an empty string if empty
func (app *OAuth2Application) PrimaryRedirectURI() string {
if len(app.RedirectURIs) == 0 {
return ""
}
return app.RedirectURIs[0]
}
// ContainsRedirectURI checks if redirectURI is allowed for app
func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
if !app.ConfidentialClient {

View File

@ -112,6 +112,15 @@ func NewAccessToken(t *AccessToken) error {
return err
}
// DisplayPublicOnly whether to display this as a public-only token.
func (t *AccessToken) DisplayPublicOnly() bool {
publicOnly, err := t.Scope.PublicOnly()
if err != nil {
return false
}
return publicOnly
}
func getAccessTokenIDFromCache(token string) int64 {
if successfulAccessTokenCache == nil {
return 0

View File

@ -6,113 +6,122 @@ package auth
import (
"fmt"
"strings"
"code.gitea.io/gitea/models/perm"
)
// AccessTokenScopeCategory represents the scope category for an access token
type AccessTokenScopeCategory int
const (
AccessTokenScopeCategoryActivityPub = iota
AccessTokenScopeCategoryAdmin
AccessTokenScopeCategoryMisc
AccessTokenScopeCategoryNotification
AccessTokenScopeCategoryOrganization
AccessTokenScopeCategoryPackage
AccessTokenScopeCategoryIssue
AccessTokenScopeCategoryRepository
AccessTokenScopeCategoryUser
)
// AllAccessTokenScopeCategories contains all access token scope categories
var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
AccessTokenScopeCategoryActivityPub,
AccessTokenScopeCategoryAdmin,
AccessTokenScopeCategoryMisc,
AccessTokenScopeCategoryNotification,
AccessTokenScopeCategoryOrganization,
AccessTokenScopeCategoryPackage,
AccessTokenScopeCategoryIssue,
AccessTokenScopeCategoryRepository,
AccessTokenScopeCategoryUser,
}
// AccessTokenScopeLevel represents the access levels without a given scope category
type AccessTokenScopeLevel int
const (
NoAccess AccessTokenScopeLevel = iota
Read
Write
)
// AccessTokenScope represents the scope for an access token.
type AccessTokenScope string
// for all categories, write implies read
const (
AccessTokenScopeAll AccessTokenScope = "all"
AccessTokenScopeAll AccessTokenScope = "all"
AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
AccessTokenScopeRepo AccessTokenScope = "repo"
AccessTokenScopeRepoStatus AccessTokenScope = "repo:status"
AccessTokenScopePublicRepo AccessTokenScope = "public_repo"
AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub"
AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
AccessTokenScopeAdminOrg AccessTokenScope = "admin:org"
AccessTokenScopeWriteOrg AccessTokenScope = "write:org"
AccessTokenScopeReadOrg AccessTokenScope = "read:org"
AccessTokenScopeReadAdmin AccessTokenScope = "read:admin"
AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin"
AccessTokenScopeAdminPublicKey AccessTokenScope = "admin:public_key"
AccessTokenScopeWritePublicKey AccessTokenScope = "write:public_key"
AccessTokenScopeReadPublicKey AccessTokenScope = "read:public_key"
AccessTokenScopeReadMisc AccessTokenScope = "read:misc"
AccessTokenScopeWriteMisc AccessTokenScope = "write:misc"
AccessTokenScopeAdminRepoHook AccessTokenScope = "admin:repo_hook"
AccessTokenScopeWriteRepoHook AccessTokenScope = "write:repo_hook"
AccessTokenScopeReadRepoHook AccessTokenScope = "read:repo_hook"
AccessTokenScopeReadNotification AccessTokenScope = "read:notification"
AccessTokenScopeWriteNotification AccessTokenScope = "write:notification"
AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook"
AccessTokenScopeReadOrganization AccessTokenScope = "read:organization"
AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization"
AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook"
AccessTokenScopeReadPackage AccessTokenScope = "read:package"
AccessTokenScopeWritePackage AccessTokenScope = "write:package"
AccessTokenScopeNotification AccessTokenScope = "notification"
AccessTokenScopeReadIssue AccessTokenScope = "read:issue"
AccessTokenScopeWriteIssue AccessTokenScope = "write:issue"
AccessTokenScopeUser AccessTokenScope = "user"
AccessTokenScopeReadUser AccessTokenScope = "read:user"
AccessTokenScopeUserEmail AccessTokenScope = "user:email"
AccessTokenScopeUserFollow AccessTokenScope = "user:follow"
AccessTokenScopeReadRepository AccessTokenScope = "read:repository"
AccessTokenScopeWriteRepository AccessTokenScope = "write:repository"
AccessTokenScopeDeleteRepo AccessTokenScope = "delete_repo"
AccessTokenScopePackage AccessTokenScope = "package"
AccessTokenScopeWritePackage AccessTokenScope = "write:package"
AccessTokenScopeReadPackage AccessTokenScope = "read:package"
AccessTokenScopeDeletePackage AccessTokenScope = "delete:package"
AccessTokenScopeAdminGPGKey AccessTokenScope = "admin:gpg_key"
AccessTokenScopeWriteGPGKey AccessTokenScope = "write:gpg_key"
AccessTokenScopeReadGPGKey AccessTokenScope = "read:gpg_key"
AccessTokenScopeAdminApplication AccessTokenScope = "admin:application"
AccessTokenScopeWriteApplication AccessTokenScope = "write:application"
AccessTokenScopeReadApplication AccessTokenScope = "read:application"
AccessTokenScopeSudo AccessTokenScope = "sudo"
AccessTokenScopeReadUser AccessTokenScope = "read:user"
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
)
// AccessTokenScopeBitmap represents a bitmap of access token scopes.
type AccessTokenScopeBitmap uint64
// accessTokenScopeBitmap represents a bitmap of access token scopes.
type accessTokenScopeBitmap uint64
// Bitmap of each scope, including the child scopes.
const (
// AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`.
AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits |
AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits |
AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits |
AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits
// AccessTokenScopeAllBits is the bitmap of all access token scopes
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
AccessTokenScopeRepoBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeRepoStatusBits | AccessTokenScopePublicRepoBits | AccessTokenScopeAdminRepoHookBits
AccessTokenScopeRepoStatusBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopePublicRepoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteOrgBits
AccessTokenScopeWriteOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadOrgBits
AccessTokenScopeReadOrgBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
AccessTokenScopeAdminPublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePublicKeyBits
AccessTokenScopeWritePublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPublicKeyBits
AccessTokenScopeReadPublicKeyBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
AccessTokenScopeAdminRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteRepoHookBits
AccessTokenScopeWriteRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadRepoHookBits
AccessTokenScopeReadRepoHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
AccessTokenScopeUserBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits
AccessTokenScopeReadUserBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeUserEmailBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeUserFollowBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
AccessTokenScopeDeleteRepoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
AccessTokenScopePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePackageBits | AccessTokenScopeDeletePackageBits
AccessTokenScopeWritePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPackageBits
AccessTokenScopeReadPackageBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeDeletePackageBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteGPGKeyBits
AccessTokenScopeWriteGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadGPGKeyBits
AccessTokenScopeReadGPGKeyBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteApplicationBits
AccessTokenScopeWriteApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadApplicationBits
AccessTokenScopeReadApplicationBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeSudoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 64 scopes,
@ -120,61 +129,110 @@ const (
)
// allAccessTokenScopes contains all access token scopes.
// The order is important: parent scope must precedes child scopes.
// The order is important: parent scope must precede child scopes.
var allAccessTokenScopes = []AccessTokenScope{
AccessTokenScopeRepo, AccessTokenScopeRepoStatus, AccessTokenScopePublicRepo,
AccessTokenScopeAdminOrg, AccessTokenScopeWriteOrg, AccessTokenScopeReadOrg,
AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey,
AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook,
AccessTokenScopeAdminOrgHook,
AccessTokenScopeAdminUserHook,
AccessTokenScopeNotification,
AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow,
AccessTokenScopeDeleteRepo,
AccessTokenScopePackage, AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, AccessTokenScopeDeletePackage,
AccessTokenScopeAdminGPGKey, AccessTokenScopeWriteGPGKey, AccessTokenScopeReadGPGKey,
AccessTokenScopeAdminApplication, AccessTokenScopeWriteApplication, AccessTokenScopeReadApplication,
AccessTokenScopeSudo,
AccessTokenScopePublicOnly,
AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub,
AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin,
AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc,
AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification,
AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization,
AccessTokenScopeWritePackage, AccessTokenScopeReadPackage,
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
}
// allAccessTokenScopeBits contains all access token scopes.
var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{
AccessTokenScopeRepo: AccessTokenScopeRepoBits,
AccessTokenScopeRepoStatus: AccessTokenScopeRepoStatusBits,
AccessTokenScopePublicRepo: AccessTokenScopePublicRepoBits,
AccessTokenScopeAdminOrg: AccessTokenScopeAdminOrgBits,
AccessTokenScopeWriteOrg: AccessTokenScopeWriteOrgBits,
AccessTokenScopeReadOrg: AccessTokenScopeReadOrgBits,
AccessTokenScopeAdminPublicKey: AccessTokenScopeAdminPublicKeyBits,
AccessTokenScopeWritePublicKey: AccessTokenScopeWritePublicKeyBits,
AccessTokenScopeReadPublicKey: AccessTokenScopeReadPublicKeyBits,
AccessTokenScopeAdminRepoHook: AccessTokenScopeAdminRepoHookBits,
AccessTokenScopeWriteRepoHook: AccessTokenScopeWriteRepoHookBits,
AccessTokenScopeReadRepoHook: AccessTokenScopeReadRepoHookBits,
AccessTokenScopeAdminOrgHook: AccessTokenScopeAdminOrgHookBits,
AccessTokenScopeAdminUserHook: AccessTokenScopeAdminUserHookBits,
AccessTokenScopeNotification: AccessTokenScopeNotificationBits,
AccessTokenScopeUser: AccessTokenScopeUserBits,
AccessTokenScopeReadUser: AccessTokenScopeReadUserBits,
AccessTokenScopeUserEmail: AccessTokenScopeUserEmailBits,
AccessTokenScopeUserFollow: AccessTokenScopeUserFollowBits,
AccessTokenScopeDeleteRepo: AccessTokenScopeDeleteRepoBits,
AccessTokenScopePackage: AccessTokenScopePackageBits,
AccessTokenScopeWritePackage: AccessTokenScopeWritePackageBits,
AccessTokenScopeReadPackage: AccessTokenScopeReadPackageBits,
AccessTokenScopeDeletePackage: AccessTokenScopeDeletePackageBits,
AccessTokenScopeAdminGPGKey: AccessTokenScopeAdminGPGKeyBits,
AccessTokenScopeWriteGPGKey: AccessTokenScopeWriteGPGKeyBits,
AccessTokenScopeReadGPGKey: AccessTokenScopeReadGPGKeyBits,
AccessTokenScopeAdminApplication: AccessTokenScopeAdminApplicationBits,
AccessTokenScopeWriteApplication: AccessTokenScopeWriteApplicationBits,
AccessTokenScopeReadApplication: AccessTokenScopeReadApplicationBits,
AccessTokenScopeSudo: AccessTokenScopeSudoBits,
var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
AccessTokenScopeAll: accessTokenScopeAllBits,
AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits,
AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits,
AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits,
AccessTokenScopeReadAdmin: accessTokenScopeReadAdminBits,
AccessTokenScopeWriteAdmin: accessTokenScopeWriteAdminBits,
AccessTokenScopeReadMisc: accessTokenScopeReadMiscBits,
AccessTokenScopeWriteMisc: accessTokenScopeWriteMiscBits,
AccessTokenScopeReadNotification: accessTokenScopeReadNotificationBits,
AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits,
AccessTokenScopeReadOrganization: accessTokenScopeReadOrganizationBits,
AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits,
AccessTokenScopeReadPackage: accessTokenScopeReadPackageBits,
AccessTokenScopeWritePackage: accessTokenScopeWritePackageBits,
AccessTokenScopeReadIssue: accessTokenScopeReadIssueBits,
AccessTokenScopeWriteIssue: accessTokenScopeWriteIssueBits,
AccessTokenScopeReadRepository: accessTokenScopeReadRepositoryBits,
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
}
// Parse parses the scope string into a bitmap, thus removing possible duplicates.
func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
var bitmap AccessTokenScopeBitmap
// readAccessTokenScopes maps a scope category to the read permission scope
var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]AccessTokenScope{
Read: {
AccessTokenScopeCategoryActivityPub: AccessTokenScopeReadActivityPub,
AccessTokenScopeCategoryAdmin: AccessTokenScopeReadAdmin,
AccessTokenScopeCategoryMisc: AccessTokenScopeReadMisc,
AccessTokenScopeCategoryNotification: AccessTokenScopeReadNotification,
AccessTokenScopeCategoryOrganization: AccessTokenScopeReadOrganization,
AccessTokenScopeCategoryPackage: AccessTokenScopeReadPackage,
AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
},
Write: {
AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
AccessTokenScopeCategoryAdmin: AccessTokenScopeWriteAdmin,
AccessTokenScopeCategoryMisc: AccessTokenScopeWriteMisc,
AccessTokenScopeCategoryNotification: AccessTokenScopeWriteNotification,
AccessTokenScopeCategoryOrganization: AccessTokenScopeWriteOrganization,
AccessTokenScopeCategoryPackage: AccessTokenScopeWritePackage,
AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
},
}
// GetRequiredScopes gets the specific scopes for a given level and categories
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
for _, cat := range scopeCategories {
scopes = append(scopes, accessTokenScopes[level][cat])
}
return scopes
}
// ContainsCategory checks if a list of categories contains a specific category
func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool {
for _, c := range categories {
if c == category {
return true
}
}
return false
}
// GetScopeLevelFromAccessMode converts permission access mode to scope level
func GetScopeLevelFromAccessMode(mode perm.AccessMode) AccessTokenScopeLevel {
switch mode {
case perm.AccessModeNone:
return NoAccess
case perm.AccessModeRead:
return Read
case perm.AccessModeWrite:
return Write
case perm.AccessModeAdmin:
return Write
case perm.AccessModeOwner:
return Write
default:
return NoAccess
}
}
// parse the scope string into a bitmap, thus removing possible duplicates.
func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
var bitmap accessTokenScopeBitmap
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
remainingScopes := string(s)
@ -196,7 +254,7 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
continue
}
if singleScope == AccessTokenScopeAll {
bitmap |= AccessTokenScopeAllBits
bitmap |= accessTokenScopeAllBits
continue
}
@ -217,26 +275,42 @@ func (s AccessTokenScope) StringSlice() []string {
// Normalize returns a normalized scope string without any duplicates.
func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
bitmap, err := s.Parse()
bitmap, err := s.parse()
if err != nil {
return "", err
}
return bitmap.ToScope(), nil
return bitmap.toScope(), nil
}
// HasScope returns true if the string has the given scope
func (s AccessTokenScope) HasScope(scope AccessTokenScope) (bool, error) {
bitmap, err := s.Parse()
// PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse()
if err != nil {
return false, err
}
return bitmap.HasScope(scope)
return bitmap.hasScope(AccessTokenScopePublicOnly)
}
// HasScope returns true if the string has the given scope
func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, error) {
func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
bitmap, err := s.parse()
if err != nil {
return false, err
}
for _, s := range scopes {
if has, err := bitmap.hasScope(s); !has || err != nil {
return has, err
}
}
return true, nil
}
// hasScope returns true if the string has the given scope
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
expectedBits, ok := allAccessTokenScopeBits[scope]
if !ok {
return false, fmt.Errorf("invalid access token scope: %s", scope)
@ -245,17 +319,17 @@ func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, err
return bitmap&expectedBits == expectedBits, nil
}
// ToScope returns a normalized scope string without any duplicates.
func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
// toScope returns a normalized scope string without any duplicates.
func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope {
var scopes []string
// iterate over all scopes, and reconstruct the bitmap
// if the reconstructed bitmap doesn't change, then the scope is already included
var reconstruct AccessTokenScopeBitmap
var reconstruct accessTokenScopeBitmap
for _, singleScope := range allAccessTokenScopes {
// no need for error checking here, since we know the scope is valid
if ok, _ := bitmap.HasScope(singleScope); ok {
if ok, _ := bitmap.hasScope(singleScope); ok {
current := reconstruct | allAccessTokenScopeBits[singleScope]
if current == reconstruct {
continue
@ -269,7 +343,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
scope := AccessTokenScope(strings.Join(scopes, ","))
scope = AccessTokenScope(strings.ReplaceAll(
string(scope),
"repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application",
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
"all",
))
return scope

View File

@ -4,44 +4,35 @@
package auth
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type scopeTestNormalize struct {
in AccessTokenScope
out AccessTokenScope
err error
}
func TestAccessTokenScope_Normalize(t *testing.T) {
tests := []struct {
in AccessTokenScope
out AccessTokenScope
err error
}{
tests := []scopeTestNormalize{
{"", "", nil},
{"repo", "repo", nil},
{"repo,repo:status", "repo", nil},
{"repo,public_repo", "repo", nil},
{"admin:public_key,write:public_key", "admin:public_key", nil},
{"admin:public_key,read:public_key", "admin:public_key", nil},
{"write:public_key,read:public_key", "write:public_key", nil}, // read is include in write
{"admin:repo_hook,write:repo_hook", "admin:repo_hook", nil},
{"admin:repo_hook,read:repo_hook", "admin:repo_hook", nil},
{"repo,admin:repo_hook,read:repo_hook", "repo", nil}, // admin:repo_hook is a child scope of repo
{"repo,read:repo_hook", "repo", nil}, // read:repo_hook is a child scope of repo
{"user", "user", nil},
{"user,read:user", "user", nil},
{"user,admin:org,write:org", "admin:org,user", nil},
{"admin:org,write:org,user", "admin:org,user", nil},
{"package", "package", nil},
{"package,write:package", "package", nil},
{"package,write:package,delete:package", "package", nil},
{"write:package,read:package", "write:package", nil}, // read is include in write
{"write:package,delete:package", "write:package,delete:package", nil}, // write and delete are not include in each other
{"admin:gpg_key", "admin:gpg_key", nil},
{"admin:gpg_key,write:gpg_key", "admin:gpg_key", nil},
{"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil},
{"admin:application,write:application,user", "user,admin:application", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
{"all", "all", nil},
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil},
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
tests = append(tests,
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
)
}
for _, test := range tests {
@ -53,31 +44,46 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
}
}
type scopeTestHasScope struct {
in AccessTokenScope
scope AccessTokenScope
out bool
err error
}
func TestAccessTokenScope_HasScope(t *testing.T) {
tests := []struct {
in AccessTokenScope
scope AccessTokenScope
out bool
err error
}{
{"repo", "repo", true, nil},
{"repo", "repo:status", true, nil},
{"repo", "public_repo", true, nil},
{"repo", "admin:org", false, nil},
{"repo", "admin:public_key", false, nil},
{"repo:status", "repo", false, nil},
{"repo:status", "public_repo", false, nil},
{"admin:org", "write:org", true, nil},
{"admin:org", "read:org", true, nil},
{"admin:org", "admin:org", true, nil},
{"user", "read:user", true, nil},
{"package", "write:package", true, nil},
tests := []scopeTestHasScope{
{"read:admin", "write:package", false, nil},
{"all", "write:package", true, nil},
{"write:package", "all", false, nil},
{"public-only", "read:issue", false, nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
tests = append(tests,
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil,
},
)
}
for _, test := range tests {
t.Run(string(test.in), func(t *testing.T) {
scope, err := test.in.HasScope(test.scope)
assert.Equal(t, test.out, scope)
hasScope, err := test.in.HasScope(test.scope)
assert.Equal(t, test.out, hasScope)
assert.Equal(t, test.err, err)
})
}

View File

@ -9,7 +9,6 @@ import (
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
// DefaultContext is the default context to run xorm queries in
@ -241,30 +240,6 @@ func TableName(bean interface{}) string {
return x.TableName(bean)
}
// EstimateCount returns an estimate of total number of rows in table
func EstimateCount(ctx context.Context, bean interface{}) (int64, error) {
e := GetEngine(ctx)
e.Context(ctx)
var rows int64
var err error
tablename := TableName(bean)
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err = e.Context(ctx).SQL("SELECT table_rows FROM information_schema.tables WHERE tables.table_name = ? AND tables.table_schema = ?;", tablename, x.Dialect().URI().DBName).Get(&rows)
case schemas.POSTGRES:
// the table can live in multiple schemas of a postgres database
// See https://wiki.postgresql.org/wiki/Count_estimate
tablename = x.TableName(bean, true)
_, err = e.Context(ctx).SQL("SELECT reltuples::bigint AS estimate FROM pg_class WHERE oid = ?::regclass;", tablename).Get(&rows)
case schemas.MSSQL:
_, err = e.Context(ctx).SQL("sp_spaceused ?;", tablename).Get(&rows)
default:
return e.Context(ctx).Count(tablename)
}
return rows, err
}
// InTransaction returns true if the engine is in a transaction otherwise return false
func InTransaction(ctx context.Context) bool {
_, ok := inTransaction(ctx)

View File

@ -34,7 +34,7 @@ func (issues IssueList) getRepoIDs() []int64 {
}
// LoadRepositories loads issues' all repositories
func (issues IssueList) LoadRepositories(ctx context.Context) ([]*repo_model.Repository, error) {
func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.RepositoryList, error) {
if len(issues) == 0 {
return nil, nil
}

View File

@ -496,6 +496,8 @@ var migrations = []Migration{
// v258 -> 259
NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue),
// v259 -> 260
NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
// v260 -> 261
NewMigration("Add Repository Licenses", v1_20.AddRepositoryLicenses),
}

View File

@ -0,0 +1,14 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_20 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View File

@ -0,0 +1,360 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_20 //nolint
import (
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"xorm.io/xorm"
)
// unknownAccessTokenScope represents the scope for an access token that isn't
// known be an old token or a new token.
type unknownAccessTokenScope string
// AccessTokenScope represents the scope for an access token.
type AccessTokenScope string
// for all categories, write implies read
const (
AccessTokenScopeAll AccessTokenScope = "all"
AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub"
AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
AccessTokenScopeReadAdmin AccessTokenScope = "read:admin"
AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin"
AccessTokenScopeReadMisc AccessTokenScope = "read:misc"
AccessTokenScopeWriteMisc AccessTokenScope = "write:misc"
AccessTokenScopeReadNotification AccessTokenScope = "read:notification"
AccessTokenScopeWriteNotification AccessTokenScope = "write:notification"
AccessTokenScopeReadOrganization AccessTokenScope = "read:organization"
AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization"
AccessTokenScopeReadPackage AccessTokenScope = "read:package"
AccessTokenScopeWritePackage AccessTokenScope = "write:package"
AccessTokenScopeReadIssue AccessTokenScope = "read:issue"
AccessTokenScopeWriteIssue AccessTokenScope = "write:issue"
AccessTokenScopeReadRepository AccessTokenScope = "read:repository"
AccessTokenScopeWriteRepository AccessTokenScope = "write:repository"
AccessTokenScopeReadUser AccessTokenScope = "read:user"
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
)
// accessTokenScopeBitmap represents a bitmap of access token scopes.
type accessTokenScopeBitmap uint64
// Bitmap of each scope, including the child scopes.
const (
// AccessTokenScopeAllBits is the bitmap of all access token scopes
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 64 scopes,
// refactoring the whole implementation in this file (and only this file) is needed.
)
// allAccessTokenScopes contains all access token scopes.
// The order is important: parent scope must precede child scopes.
var allAccessTokenScopes = []AccessTokenScope{
AccessTokenScopePublicOnly,
AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub,
AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin,
AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc,
AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification,
AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization,
AccessTokenScopeWritePackage, AccessTokenScopeReadPackage,
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
}
// allAccessTokenScopeBits contains all access token scopes.
var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
AccessTokenScopeAll: accessTokenScopeAllBits,
AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits,
AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits,
AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits,
AccessTokenScopeReadAdmin: accessTokenScopeReadAdminBits,
AccessTokenScopeWriteAdmin: accessTokenScopeWriteAdminBits,
AccessTokenScopeReadMisc: accessTokenScopeReadMiscBits,
AccessTokenScopeWriteMisc: accessTokenScopeWriteMiscBits,
AccessTokenScopeReadNotification: accessTokenScopeReadNotificationBits,
AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits,
AccessTokenScopeReadOrganization: accessTokenScopeReadOrganizationBits,
AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits,
AccessTokenScopeReadPackage: accessTokenScopeReadPackageBits,
AccessTokenScopeWritePackage: accessTokenScopeWritePackageBits,
AccessTokenScopeReadIssue: accessTokenScopeReadIssueBits,
AccessTokenScopeWriteIssue: accessTokenScopeWriteIssueBits,
AccessTokenScopeReadRepository: accessTokenScopeReadRepositoryBits,
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
}
// hasScope returns true if the string has the given scope
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
expectedBits, ok := allAccessTokenScopeBits[scope]
if !ok {
return false, fmt.Errorf("invalid access token scope: %s", scope)
}
return bitmap&expectedBits == expectedBits, nil
}
// toScope returns a normalized scope string without any duplicates.
func (bitmap accessTokenScopeBitmap) toScope(unknownScopes *[]unknownAccessTokenScope) AccessTokenScope {
var scopes []string
// Preserve unknown scopes, and put them at the beginning so that it's clear
// when debugging.
if unknownScopes != nil {
for _, unknownScope := range *unknownScopes {
scopes = append(scopes, string(unknownScope))
}
}
// iterate over all scopes, and reconstruct the bitmap
// if the reconstructed bitmap doesn't change, then the scope is already included
var reconstruct accessTokenScopeBitmap
for _, singleScope := range allAccessTokenScopes {
// no need for error checking here, since we know the scope is valid
if ok, _ := bitmap.hasScope(singleScope); ok {
current := reconstruct | allAccessTokenScopeBits[singleScope]
if current == reconstruct {
continue
}
reconstruct = current
scopes = append(scopes, string(singleScope))
}
}
scope := AccessTokenScope(strings.Join(scopes, ","))
scope = AccessTokenScope(strings.ReplaceAll(
string(scope),
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
"all",
))
return scope
}
// parse the scope string into a bitmap, thus removing possible duplicates.
func (s AccessTokenScope) parse() (accessTokenScopeBitmap, *[]unknownAccessTokenScope) {
var bitmap accessTokenScopeBitmap
var unknownScopes []unknownAccessTokenScope
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
remainingScopes := string(s)
for len(remainingScopes) > 0 {
i := strings.IndexByte(remainingScopes, ',')
var v string
if i < 0 {
v = remainingScopes
remainingScopes = ""
} else if i+1 >= len(remainingScopes) {
v = remainingScopes[:i]
remainingScopes = ""
} else {
v = remainingScopes[:i]
remainingScopes = remainingScopes[i+1:]
}
singleScope := AccessTokenScope(v)
if singleScope == "" {
continue
}
if singleScope == AccessTokenScopeAll {
bitmap |= accessTokenScopeAllBits
continue
}
bits, ok := allAccessTokenScopeBits[singleScope]
if !ok {
unknownScopes = append(unknownScopes, unknownAccessTokenScope(string(singleScope)))
}
bitmap |= bits
}
return bitmap, &unknownScopes
}
// NormalizePreservingUnknown returns a normalized scope string without any
// duplicates. Unknown scopes are included.
func (s AccessTokenScope) NormalizePreservingUnknown() AccessTokenScope {
bitmap, unknownScopes := s.parse()
return bitmap.toScope(unknownScopes)
}
// OldAccessTokenScope represents the scope for an access token.
type OldAccessTokenScope string
const (
OldAccessTokenScopeAll OldAccessTokenScope = "all"
OldAccessTokenScopeRepo OldAccessTokenScope = "repo"
OldAccessTokenScopeRepoStatus OldAccessTokenScope = "repo:status"
OldAccessTokenScopePublicRepo OldAccessTokenScope = "public_repo"
OldAccessTokenScopeAdminOrg OldAccessTokenScope = "admin:org"
OldAccessTokenScopeWriteOrg OldAccessTokenScope = "write:org"
OldAccessTokenScopeReadOrg OldAccessTokenScope = "read:org"
OldAccessTokenScopeAdminPublicKey OldAccessTokenScope = "admin:public_key"
OldAccessTokenScopeWritePublicKey OldAccessTokenScope = "write:public_key"
OldAccessTokenScopeReadPublicKey OldAccessTokenScope = "read:public_key"
OldAccessTokenScopeAdminRepoHook OldAccessTokenScope = "admin:repo_hook"
OldAccessTokenScopeWriteRepoHook OldAccessTokenScope = "write:repo_hook"
OldAccessTokenScopeReadRepoHook OldAccessTokenScope = "read:repo_hook"
OldAccessTokenScopeAdminOrgHook OldAccessTokenScope = "admin:org_hook"
OldAccessTokenScopeNotification OldAccessTokenScope = "notification"
OldAccessTokenScopeUser OldAccessTokenScope = "user"
OldAccessTokenScopeReadUser OldAccessTokenScope = "read:user"
OldAccessTokenScopeUserEmail OldAccessTokenScope = "user:email"
OldAccessTokenScopeUserFollow OldAccessTokenScope = "user:follow"
OldAccessTokenScopeDeleteRepo OldAccessTokenScope = "delete_repo"
OldAccessTokenScopePackage OldAccessTokenScope = "package"
OldAccessTokenScopeWritePackage OldAccessTokenScope = "write:package"
OldAccessTokenScopeReadPackage OldAccessTokenScope = "read:package"
OldAccessTokenScopeDeletePackage OldAccessTokenScope = "delete:package"
OldAccessTokenScopeAdminGPGKey OldAccessTokenScope = "admin:gpg_key"
OldAccessTokenScopeWriteGPGKey OldAccessTokenScope = "write:gpg_key"
OldAccessTokenScopeReadGPGKey OldAccessTokenScope = "read:gpg_key"
OldAccessTokenScopeAdminApplication OldAccessTokenScope = "admin:application"
OldAccessTokenScopeWriteApplication OldAccessTokenScope = "write:application"
OldAccessTokenScopeReadApplication OldAccessTokenScope = "read:application"
OldAccessTokenScopeSudo OldAccessTokenScope = "sudo"
)
var accessTokenScopeMap = map[OldAccessTokenScope][]AccessTokenScope{
OldAccessTokenScopeAll: {AccessTokenScopeAll},
OldAccessTokenScopeRepo: {AccessTokenScopeWriteRepository},
OldAccessTokenScopeRepoStatus: {AccessTokenScopeWriteRepository},
OldAccessTokenScopePublicRepo: {AccessTokenScopePublicOnly, AccessTokenScopeWriteRepository},
OldAccessTokenScopeAdminOrg: {AccessTokenScopeWriteOrganization},
OldAccessTokenScopeWriteOrg: {AccessTokenScopeWriteOrganization},
OldAccessTokenScopeReadOrg: {AccessTokenScopeReadOrganization},
OldAccessTokenScopeAdminPublicKey: {AccessTokenScopeWriteUser},
OldAccessTokenScopeWritePublicKey: {AccessTokenScopeWriteUser},
OldAccessTokenScopeReadPublicKey: {AccessTokenScopeReadUser},
OldAccessTokenScopeAdminRepoHook: {AccessTokenScopeWriteRepository},
OldAccessTokenScopeWriteRepoHook: {AccessTokenScopeWriteRepository},
OldAccessTokenScopeReadRepoHook: {AccessTokenScopeReadRepository},
OldAccessTokenScopeAdminOrgHook: {AccessTokenScopeWriteOrganization},
OldAccessTokenScopeNotification: {AccessTokenScopeWriteNotification},
OldAccessTokenScopeUser: {AccessTokenScopeWriteUser},
OldAccessTokenScopeReadUser: {AccessTokenScopeReadUser},
OldAccessTokenScopeUserEmail: {AccessTokenScopeWriteUser},
OldAccessTokenScopeUserFollow: {AccessTokenScopeWriteUser},
OldAccessTokenScopeDeleteRepo: {AccessTokenScopeWriteRepository},
OldAccessTokenScopePackage: {AccessTokenScopeWritePackage},
OldAccessTokenScopeWritePackage: {AccessTokenScopeWritePackage},
OldAccessTokenScopeReadPackage: {AccessTokenScopeReadPackage},
OldAccessTokenScopeDeletePackage: {AccessTokenScopeWritePackage},
OldAccessTokenScopeAdminGPGKey: {AccessTokenScopeWriteUser},
OldAccessTokenScopeWriteGPGKey: {AccessTokenScopeWriteUser},
OldAccessTokenScopeReadGPGKey: {AccessTokenScopeReadUser},
OldAccessTokenScopeAdminApplication: {AccessTokenScopeWriteUser},
OldAccessTokenScopeWriteApplication: {AccessTokenScopeWriteUser},
OldAccessTokenScopeReadApplication: {AccessTokenScopeReadUser},
OldAccessTokenScopeSudo: {AccessTokenScopeWriteAdmin},
}
type AccessToken struct {
ID int64 `xorm:"pk autoincr"`
Scope string
}
func ConvertScopedAccessTokens(x *xorm.Engine) error {
var tokens []*AccessToken
if err := x.Find(&tokens); err != nil {
return err
}
for _, token := range tokens {
var scopes []string
allNewScopesMap := make(map[AccessTokenScope]bool)
for _, oldScope := range strings.Split(token.Scope, ",") {
if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists {
for _, newScope := range newScopes {
allNewScopesMap[newScope] = true
}
} else {
log.Debug("access token scope not recognized as old token scope %s; preserving it", oldScope)
scopes = append(scopes, oldScope)
}
}
for s := range allNewScopesMap {
scopes = append(scopes, string(s))
}
scope := AccessTokenScope(strings.Join(scopes, ","))
// normalize the scope
normScope := scope.NormalizePreservingUnknown()
token.Scope = string(normScope)
// update the db entry with the new scope
if _, err := x.Cols("scope").ID(token.ID).Update(token); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,110 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_20 //nolint
import (
"sort"
"strings"
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
)
type testCase struct {
Old OldAccessTokenScope
New AccessTokenScope
}
func createOldTokenScope(scopes ...OldAccessTokenScope) OldAccessTokenScope {
s := make([]string, 0, len(scopes))
for _, os := range scopes {
s = append(s, string(os))
}
return OldAccessTokenScope(strings.Join(s, ","))
}
func createNewTokenScope(scopes ...AccessTokenScope) AccessTokenScope {
s := make([]string, 0, len(scopes))
for _, os := range scopes {
s = append(s, string(os))
}
return AccessTokenScope(strings.Join(s, ","))
}
func Test_ConvertScopedAccessTokens(t *testing.T) {
tests := []testCase{
{
createOldTokenScope(OldAccessTokenScopeRepo, OldAccessTokenScopeUserFollow),
createNewTokenScope(AccessTokenScopeWriteRepository, AccessTokenScopeWriteUser),
},
{
createOldTokenScope(OldAccessTokenScopeUser, OldAccessTokenScopeWritePackage, OldAccessTokenScopeSudo),
createNewTokenScope(AccessTokenScopeWriteAdmin, AccessTokenScopeWritePackage, AccessTokenScopeWriteUser),
},
{
createOldTokenScope(),
createNewTokenScope(),
},
{
createOldTokenScope(OldAccessTokenScopeReadGPGKey, OldAccessTokenScopeReadOrg, OldAccessTokenScopeAll),
createNewTokenScope(AccessTokenScopeAll),
},
{
createOldTokenScope(OldAccessTokenScopeReadGPGKey, "invalid"),
createNewTokenScope("invalid", AccessTokenScopeReadUser),
},
}
// add a test for each individual mapping
for oldScope, newScope := range accessTokenScopeMap {
tests = append(tests, testCase{
oldScope,
createNewTokenScope(newScope...),
})
}
x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken))
defer deferable()
if x == nil || t.Failed() {
t.Skip()
return
}
// verify that no fixtures were loaded
count, err := x.Count(&AccessToken{})
assert.NoError(t, err)
assert.Equal(t, int64(0), count)
for _, tc := range tests {
_, err = x.Insert(&AccessToken{
Scope: string(tc.Old),
})
assert.NoError(t, err)
}
// migrate the scopes
err = ConvertScopedAccessTokens(x)
assert.NoError(t, err)
// migrate the scopes again (migration should be idempotent)
err = ConvertScopedAccessTokens(x)
assert.NoError(t, err)
tokens := make([]AccessToken, 0)
err = x.Find(&tokens)
assert.NoError(t, err)
assert.Equal(t, len(tests), len(tokens))
// sort the tokens (insertion order by auto-incrementing primary key)
sort.Slice(tokens, func(i, j int) bool {
return tokens[i].ID < tokens[j].ID
})
// verify that the converted scopes are equal to the expected test result
for idx, newToken := range tokens {
assert.Equal(t, string(tests[idx].New), newToken.Scope)
}
}

View File

@ -710,8 +710,8 @@ func (org *Organization) GetUserTeams(userID int64) ([]*Team, error) {
type AccessibleReposEnvironment interface {
CountRepos() (int64, error)
RepoIDs(page, pageSize int) ([]int64, error)
Repos(page, pageSize int) ([]*repo_model.Repository, error)
MirrorRepos() ([]*repo_model.Repository, error)
Repos(page, pageSize int) (repo_model.RepositoryList, error)
MirrorRepos() (repo_model.RepositoryList, error)
AddKeyword(keyword string)
SetSort(db.SearchOrderBy)
}
@ -813,7 +813,7 @@ func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) {
Find(&repoIDs)
}
func (env *accessibleReposEnv) Repos(page, pageSize int) ([]*repo_model.Repository, error) {
func (env *accessibleReposEnv) Repos(page, pageSize int) (repo_model.RepositoryList, error) {
repoIDs, err := env.RepoIDs(page, pageSize)
if err != nil {
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
@ -842,7 +842,7 @@ func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) {
Find(&repoIDs)
}
func (env *accessibleReposEnv) MirrorRepos() ([]*repo_model.Repository, error) {
func (env *accessibleReposEnv) MirrorRepos() (repo_model.RepositoryList, error) {
repoIDs, err := env.MirrorRepoIDs()
if err != nil {
return nil, fmt.Errorf("MirrorRepoIDs: %w", err)

View File

@ -11,7 +11,7 @@ import (
)
// GetOrgRepositories get repos belonging to the given organization
func GetOrgRepositories(ctx context.Context, orgID int64) ([]*repo_model.Repository, error) {
func GetOrgRepositories(ctx context.Context, orgID int64) (repo_model.RepositoryList, error) {
var orgRepos []*repo_model.Repository
return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos)
}

View File

@ -346,7 +346,7 @@ func TestAccessibleReposEnv_Repos(t *testing.T) {
assert.NoError(t, err)
repos, err := env.Repos(1, 100)
assert.NoError(t, err)
expectedRepos := make([]*repo_model.Repository, len(expectedRepoIDs))
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
for i, repoID := range expectedRepoIDs {
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
&repo_model.Repository{ID: repoID})
@ -365,7 +365,7 @@ func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
assert.NoError(t, err)
repos, err := env.MirrorRepos()
assert.NoError(t, err)
expectedRepos := make([]*repo_model.Repository, len(expectedRepoIDs))
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
for i, repoID := range expectedRepoIDs {
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
&repo_model.Repository{ID: repoID})

View File

@ -37,7 +37,7 @@ type SearchTeamRepoOptions struct {
}
// GetRepositories returns paginated repositories in team of organization.
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) ([]*repo_model.Repository, error) {
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo_model.RepositoryList, error) {
sess := db.GetEngine(ctx)
if opts.TeamID > 0 {
sess = sess.In("id",

View File

@ -111,28 +111,36 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
}
}
// RequireRepoScopedToken check whether personal access token has repo scope
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) {
// CheckRepoScopedToken check whether personal access token has repo scope
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
return
}
var err error
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ok { // it's a personal access token but not oauth2 token
var scopeMatched bool
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo)
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if !scopeMatched && !repo.IsPrivate {
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo)
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if publicOnly && repo.IsPrivate {
ctx.Error(http.StatusForbidden)
return
}
scopeMatched, err = scope.HasScope(requiredScopes...)
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if !scopeMatched {
ctx.Error(http.StatusForbidden)
return

View File

@ -18,7 +18,6 @@ const namespace = "gitea_"
// exposes gitea metrics for prometheus
type Collector struct {
Accesses *prometheus.Desc
Actions *prometheus.Desc
Attachments *prometheus.Desc
BuildInfo *prometheus.Desc
Comments *prometheus.Desc
@ -56,11 +55,6 @@ func NewCollector() Collector {
"Number of Accesses",
nil, nil,
),
Actions: prometheus.NewDesc(
namespace+"actions",
"Number of Actions",
nil, nil,
),
Attachments: prometheus.NewDesc(
namespace+"attachments",
"Number of Attachments",
@ -207,7 +201,6 @@ func NewCollector() Collector {
// Describe returns all possible prometheus.Desc
func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Accesses
ch <- c.Actions
ch <- c.Attachments
ch <- c.BuildInfo
ch <- c.Comments
@ -246,11 +239,6 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
prometheus.GaugeValue,
float64(stats.Counter.Access),
)
ch <- prometheus.MustNewConstMetric(
c.Actions,
prometheus.GaugeValue,
float64(stats.Counter.Action),
)
ch <- prometheus.MustNewConstMetric(
c.Attachments,
prometheus.GaugeValue,

View File

@ -28,7 +28,7 @@ type Notifier interface {
NotifyDeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue)
NotifyIssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64)
NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment)
NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment)
NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment)
NotifyIssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string)
NotifyIssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue)
NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string)

View File

@ -120,8 +120,8 @@ func (*NullNotifier) NotifyIssueChangeContent(ctx context.Context, doer *user_mo
func (*NullNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
}
// NotifyPullReviewRequest places a place holder function
func (*NullNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
// NotifyPullRequestReviewRequest places a place holder function
func (*NullNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
}
// NotifyIssueClearLabels places a place holder function

View File

@ -123,7 +123,7 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user
}
}
func (m *mailNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
func (m *mailNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() != user_model.EmailNotificationsDisabled {
ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL())
if err := mailer.SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil {

View File

@ -230,10 +230,10 @@ func NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue
}
}
// NotifyPullReviewRequest notifies Request Review change
func NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
// NotifyPullRequestReviewRequest notifies Request Review change
func NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
for _, notifier := range notifiers {
notifier.NotifyPullReviewRequest(ctx, doer, issue, reviewer, isRequest, comment)
notifier.NotifyPullRequestReviewRequest(ctx, doer, issue, reviewer, isRequest, comment)
}
}

View File

@ -229,7 +229,7 @@ func (ns *notificationService) NotifyIssueChangeAssignee(ctx context.Context, do
}
}
func (ns *notificationService) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
func (ns *notificationService) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
if isRequest {
opts := issueNotificationOpts{
IssueID: issue.ID,

View File

@ -27,7 +27,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gopkg.in/ini.v1"
"gopkg.in/ini.v1" //nolint:depguard
)
/*
@ -247,7 +247,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
// This also removes possible user credentials.
func cleanUpMigrateGitConfig(configPath string) error {
cfg, err := ini.Load(configPath)
cfg, err := ini.Load(configPath) // FIXME: the ini package doesn't really work with git config files
if err != nil {
return fmt.Errorf("open config file: %w", err)
}

View File

@ -10,8 +10,6 @@ import (
"strings"
"code.gitea.io/gitea/modules/log"
"gopkg.in/ini.v1"
)
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
@ -89,7 +87,7 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
return ok, section, key, useFileValue
}
func EnvironmentToConfig(cfg *ini.File, prefixGitea, suffixFile string, envs []string) (changed bool) {
func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, envs []string) (changed bool) {
for _, kv := range envs {
idx := strings.IndexByte(kv, '=')
if idx < 0 {

View File

@ -8,7 +8,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/ini.v1"
)
func TestDecodeEnvSectionKey(t *testing.T) {
@ -71,15 +70,15 @@ func TestDecodeEnvironmentKey(t *testing.T) {
}
func TestEnvironmentToConfig(t *testing.T) {
cfg := ini.Empty()
cfg, _ := NewConfigProviderFromData("")
changed := EnvironmentToConfig(cfg, "GITEA__", "__FILE", nil)
assert.False(t, changed)
cfg, err := ini.Load([]byte(`
cfg, err := NewConfigProviderFromData(`
[sec]
key = old
`))
`)
assert.NoError(t, err)
changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})

View File

@ -8,36 +8,71 @@ import (
"os"
"path/filepath"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
ini "gopkg.in/ini.v1"
"gopkg.in/ini.v1" //nolint:depguard
)
type ConfigKey interface {
Name() string
Value() string
SetValue(v string)
In(defaultVal string, candidates []string) string
String() string
Strings(delim string) []string
MustString(defaultVal string) string
MustBool(defaultVal ...bool) bool
MustInt(defaultVal ...int) int
MustInt64(defaultVal ...int64) int64
MustDuration(defaultVal ...time.Duration) time.Duration
}
type ConfigSection interface {
Name() string
MapTo(interface{}) error
MapTo(any) error
HasKey(key string) bool
NewKey(name, value string) (*ini.Key, error)
Key(key string) *ini.Key
Keys() []*ini.Key
ChildSections() []*ini.Section
NewKey(name, value string) (ConfigKey, error)
Key(key string) ConfigKey
Keys() []ConfigKey
ChildSections() []ConfigSection
}
// ConfigProvider represents a config provider
type ConfigProvider interface {
Section(section string) ConfigSection
Sections() []ConfigSection
NewSection(name string) (ConfigSection, error)
GetSection(name string) (ConfigSection, error)
Save() error
SaveTo(filename string) error
}
type iniConfigProvider struct {
opts *Options
ini *ini.File
newFile bool // whether the file has not existed previously
}
type iniConfigSection struct {
sec *ini.Section
}
var (
_ ConfigProvider = (*iniConfigProvider)(nil)
_ ConfigSection = (*iniConfigSection)(nil)
_ ConfigKey = (*ini.Key)(nil)
)
// ConfigSectionKey only searches the keys in the given section, but it is O(n).
// ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]",
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
// It returns nil if the key doesn't exist.
func ConfigSectionKey(sec ConfigSection, key string) *ini.Key {
func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
if sec == nil {
return nil
}
@ -64,7 +99,7 @@ func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
// It never returns nil.
func ConfigInheritedKey(sec ConfigSection, key string) *ini.Key {
func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
k := sec.Key(key)
if k != nil && k.String() != "" {
newKey, _ := sec.NewKey(k.Name(), k.String())
@ -85,41 +120,64 @@ func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) stri
return ""
}
type iniFileConfigProvider struct {
opts *Options
*ini.File
newFile bool // whether the file has not existed previously
func (s *iniConfigSection) Name() string {
return s.sec.Name()
}
// NewConfigProviderFromData this function is only for testing
func (s *iniConfigSection) MapTo(v any) error {
return s.sec.MapTo(v)
}
func (s *iniConfigSection) HasKey(key string) bool {
return s.sec.HasKey(key)
}
func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
return s.sec.NewKey(name, value)
}
func (s *iniConfigSection) Key(key string) ConfigKey {
return s.sec.Key(key)
}
func (s *iniConfigSection) Keys() (keys []ConfigKey) {
for _, k := range s.sec.Keys() {
keys = append(keys, k)
}
return keys
}
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
for _, s := range s.sec.ChildSections() {
sections = append(sections, &iniConfigSection{s})
}
return sections
}
// NewConfigProviderFromData this function is mainly for testing purpose
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
var cfg *ini.File
var err error
if configContent == "" {
cfg = ini.Empty()
} else {
cfg, err = ini.Load(strings.NewReader(configContent))
if err != nil {
return nil, err
}
cfg, err := ini.Load(strings.NewReader(configContent))
if err != nil {
return nil, err
}
cfg.NameMapper = ini.SnackCase
return &iniFileConfigProvider{
File: cfg,
return &iniConfigProvider{
ini: cfg,
newFile: true,
}, nil
}
type Options struct {
CustomConf string // the ini file path
AllowEmpty bool // whether not finding configuration files is allowed (only true for the tests)
ExtraConfig string
DisableLoadCommonSettings bool
CustomConf string // the ini file path
AllowEmpty bool // whether not finding configuration files is allowed
ExtraConfig string
DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()"
}
// newConfigProviderFromFile load configuration from file.
// NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error.
func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) {
func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) {
cfg := ini.Empty()
newFile := true
@ -147,61 +205,77 @@ func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) {
}
cfg.NameMapper = ini.SnackCase
return &iniFileConfigProvider{
return &iniConfigProvider{
opts: opts,
File: cfg,
ini: cfg,
newFile: newFile,
}, nil
}
func (p *iniFileConfigProvider) Section(section string) ConfigSection {
return p.File.Section(section)
func (p *iniConfigProvider) Section(section string) ConfigSection {
return &iniConfigSection{sec: p.ini.Section(section)}
}
func (p *iniFileConfigProvider) NewSection(name string) (ConfigSection, error) {
return p.File.NewSection(name)
func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
for _, s := range p.ini.Sections() {
sections = append(sections, &iniConfigSection{s})
}
return sections
}
func (p *iniFileConfigProvider) GetSection(name string) (ConfigSection, error) {
return p.File.GetSection(name)
func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
sec, err := p.ini.NewSection(name)
if err != nil {
return nil, err
}
return &iniConfigSection{sec: sec}, nil
}
// Save save the content into file
func (p *iniFileConfigProvider) Save() error {
if p.opts.CustomConf == "" {
func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
sec, err := p.ini.GetSection(name)
if err != nil {
return nil, err
}
return &iniConfigSection{sec: sec}, nil
}
// Save saves the content into file
func (p *iniConfigProvider) Save() error {
filename := p.opts.CustomConf
if filename == "" {
if !p.opts.AllowEmpty {
return fmt.Errorf("custom config path must not be empty")
}
return nil
}
if p.newFile {
if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
return fmt.Errorf("failed to create '%s': %v", CustomConf, err)
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
return fmt.Errorf("failed to create '%s': %v", filename, err)
}
}
if err := p.SaveTo(p.opts.CustomConf); err != nil {
return fmt.Errorf("failed to save '%s': %v", p.opts.CustomConf, err)
if err := p.ini.SaveTo(filename); err != nil {
return fmt.Errorf("failed to save '%s': %v", filename, err)
}
// Change permissions to be more restrictive
fi, err := os.Stat(CustomConf)
fi, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("failed to determine current conf file permissions: %v", err)
}
if fi.Mode().Perm() > 0o600 {
if err = os.Chmod(CustomConf, 0o600); err != nil {
if err = os.Chmod(filename, 0o600); err != nil {
log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
}
}
return nil
}
// a file is an implementation ConfigProvider and other implementations are possible, i.e. from docker, k8s, …
var _ ConfigProvider = &iniFileConfigProvider{}
func (p *iniConfigProvider) SaveTo(filename string) error {
return p.ini.SaveTo(filename)
}
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting interface{}) {
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
log.Fatal("Failed to map %s settings: %v", sectionName, err)
}
@ -219,3 +293,23 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
}
}
// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content
func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) {
iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, source, others...)
if err != nil {
return nil, fmt.Errorf("unable to load locale ini: %w", err)
}
iniFile.BlockMode = false
return &iniConfigProvider{
ini: iniFile,
newFile: true,
}, nil
}
func init() {
ini.PrettyFormat = false
}

View File

@ -4,6 +4,7 @@
package setting
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -64,3 +65,57 @@ key = 123
assert.Equal(t, "", ConfigSectionKeyString(sec, "empty"))
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "empty"))
}
func TestNewConfigProviderFromFile(t *testing.T) {
_, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false})
assert.ErrorContains(t, err, "unable to find configuration file")
// load non-existing file and save
testFile := t.TempDir() + "/test.ini"
testFile1 := t.TempDir() + "/test1.ini"
cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
assert.NoError(t, err)
sec, _ := cfg.NewSection("foo")
_, _ = sec.NewKey("k1", "a")
assert.NoError(t, cfg.Save())
_, _ = sec.NewKey("k2", "b")
assert.NoError(t, cfg.SaveTo(testFile1))
bs, err := os.ReadFile(testFile)
assert.NoError(t, err)
assert.Equal(t, "[foo]\nk1=a\n", string(bs))
bs, err = os.ReadFile(testFile1)
assert.NoError(t, err)
assert.Equal(t, "[foo]\nk1=a\nk2=b\n", string(bs))
// load existing file and save
cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
assert.NoError(t, err)
assert.Equal(t, "a", cfg.Section("foo").Key("k1").String())
sec, _ = cfg.NewSection("bar")
_, _ = sec.NewKey("k1", "b")
assert.NoError(t, cfg.Save())
bs, err = os.ReadFile(testFile)
assert.NoError(t, err)
assert.Equal(t, "[foo]\nk1=a\n\n[bar]\nk1=b\n", string(bs))
}
func TestNewConfigProviderForLocale(t *testing.T) {
// load locale from file
localeFile := t.TempDir() + "/locale.ini"
_ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644)
cfg, err := NewConfigProviderForLocale(localeFile)
assert.NoError(t, err)
assert.Equal(t, "a", cfg.Section("").Key("k1").String())
// load locale from bytes
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"))
assert.NoError(t, err)
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx"))
assert.NoError(t, err)
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
assert.Equal(t, "xxx", cfg.Section("").Key("k2").String())
}

View File

@ -201,7 +201,7 @@ func Init(opts *Options) {
opts.CustomConf = CustomConf
}
var err error
CfgProvider, err = newConfigProviderFromFile(opts)
CfgProvider, err = NewConfigProviderFromFile(opts)
if err != nil {
log.Fatal("Init[%v]: %v", opts, err)
}

View File

@ -32,7 +32,6 @@ var UI = struct {
CustomEmojis []string
CustomEmojisMap map[string]string `ini:"-"`
SearchRepoDescription bool
UseServiceWorker bool
OnlyShowRelevantRepos bool
Notification struct {
@ -136,7 +135,6 @@ func loadUIFrom(rootCfg ConfigProvider) {
UI.ShowUserEmail = sec.Key("SHOW_USER_EMAIL").MustBool(true)
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true)
UI.UseServiceWorker = sec.Key("USE_SERVICE_WORKER").MustBool(false)
// OnlyShowRelevantRepos=false is important for many private/enterprise instances,
// because many private repositories do not have "description/topic", users just want to search by their names.

View File

@ -127,9 +127,6 @@ func NewFuncMap() template.FuncMap {
"MetaKeywords": func() string {
return setting.UI.Meta.Keywords
},
"UseServiceWorker": func() bool {
return setting.UI.UseServiceWorker
},
"EnableTimetracking": func() bool {
return setting.Service.EnableTimetracking
},

View File

@ -7,8 +7,7 @@ import (
"fmt"
"code.gitea.io/gitea/modules/log"
"gopkg.in/ini.v1"
"code.gitea.io/gitea/modules/setting"
)
// This file implements the static LocaleStore that will not watch for changes
@ -47,14 +46,10 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
store.localeMap[l.langName] = l
iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, source, moreSource)
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
if err != nil {
return fmt.Errorf("unable to load ini: %w", err)
}
iniFile.BlockMode = false
for _, section := range iniFile.Sections() {
for _, key := range section.Keys() {

View File

@ -3,7 +3,10 @@
package util
import "unicode/utf8"
import (
"strings"
"unicode/utf8"
)
// in UTF8 "…" is 3 bytes so doesn't really gain us anything...
const (
@ -35,3 +38,17 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
}
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
func SplitTrimSpace(input, sep string) []string {
// replace CRLF with LF
input = strings.ReplaceAll(input, "\r\n", "\n")
var stringList []string
for _, s := range strings.Split(input, sep) {
// trim leading and trailing space
stringList = append(stringList, strings.TrimSpace(s))
}
return stringList
}

View File

@ -1182,7 +1182,7 @@ issues.add_label=hat das Label %s %s hinzugefügt
issues.add_labels=hat die Labels %s %s hinzugefügt
issues.remove_label=hat das Label %s %s entfernt
issues.remove_labels=hat die Labels %s %s entfernt
issues.add_remove_labels=hat %s hinzugefügt, und %s %s enternt
issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt
issues.add_milestone_at=`hat diesen Issue %[2]s zum <b>%[1]s</b> Meilenstein hinzugefügt`
issues.add_project_at=`hat dieses zum <b>%s</b> projekt %s hinzugefügt`
issues.change_milestone_at=`hat den Meilenstein %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`

View File

@ -796,7 +796,6 @@ unbind_success = The social account has been unlinked from your Gitea account.
manage_access_token = Manage Access Tokens
generate_new_token = Generate New Token
tokens_desc = These tokens grant access to your account using the Gitea API.
new_token_desc = Applications using a token have full access to your account.
token_name = Token Name
generate_token = Generate Token
generate_token_success = Your new token has been generated. Copy it now as it will not be shown again.
@ -807,8 +806,13 @@ access_token_deletion_cancel_action = Cancel
access_token_deletion_confirm_action = Delete
access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?
delete_token_success = The token has been deleted. Applications using it no longer have access to your account.
select_scopes = Select scopes
scopes_list = Scopes:
repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only
permissions_access_all = All (public, private, and limited)
select_permissions = Select permissions
scoped_token_desc = Selected token scopes limit authentication only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
at_least_one_permission = You must select at least one permission to create a token
permissions_list = Permissions:
manage_oauth2_applications = Manage OAuth2 Applications
edit_oauth2_application = Edit OAuth2 Application
@ -822,7 +826,7 @@ create_oauth2_application_success = You've successfully created a new OAuth2 app
update_oauth2_application_success = You've successfully updated the OAuth2 application.
oauth2_application_name = Application Name
oauth2_confidential_client = Confidential Client. Select for apps that keep the secret confidential, such as web apps. Do not select for native apps including desktop and mobile apps.
oauth2_redirect_uri = Redirect URI
oauth2_redirect_uris = Redirect URIs. Please use a new line for every URI.
save_application = Save
oauth2_client_id = Client ID
oauth2_client_secret = Client Secret
@ -1756,7 +1760,7 @@ milestones.no_due_date = No due date
milestones.open = Open
milestones.close = Close
milestones.new_subheader = Milestones organize issues and track progress.
milestones.completeness = %d%% Completed
milestones.completeness = <strong>%d%%</strong> Completed
milestones.create = Create Milestone
milestones.title = Title
milestones.desc = Description
@ -2620,7 +2624,6 @@ dashboard.new_version_hint = Gitea %s is now available, you are running %s. Chec
dashboard.statistic = Summary
dashboard.operations = Maintenance Operations
dashboard.system_status = System Status
dashboard.statistic_info = The Gitea database holds <b>%d</b> users, <b>%d</b> organizations, <b>%d</b> public keys, <b>%d</b> repositories, <b>%d</b> watches, <b>%d</b> stars, ~<b>%d</b> actions, <b>%d</b> accesses, <b>%d</b> issues, <b>%d</b> comments, <b>%d</b> social accounts, <b>%d</b> follows, <b>%d</b> mirrors, <b>%d</b> releases, <b>%d</b> authentication sources, <b>%d</b> webhooks, <b>%d</b> milestones, <b>%d</b> labels, <b>%d</b> hook tasks, <b>%d</b> teams, <b>%d</b> update tasks, <b>%d</b> attachments.
dashboard.operation_name = Operation Name
dashboard.operation_switch = Switch
dashboard.operation_run = Run
@ -3061,6 +3064,8 @@ config.xorm_log_sql = Log SQL
config.get_setting_failed = Get setting %s failed
config.set_setting_failed = Set setting %s failed
monitor.stats = Stats
monitor.cron = Cron Tasks
monitor.name = Name
monitor.schedule = Schedule

23
package-lock.json generated
View File

@ -51,8 +51,6 @@
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.84.1",
"webpack-cli": "5.1.1",
"workbox-routing": "6.6.0",
"workbox-strategies": "6.6.0",
"wrap-ansi": "8.1.0"
},
"devDependencies": {
@ -11045,27 +11043,6 @@
"node": ">=0.10.0"
}
},
"node_modules/workbox-core": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz",
"integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ=="
},
"node_modules/workbox-routing": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz",
"integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==",
"dependencies": {
"workbox-core": "6.6.0"
}
},
"node_modules/workbox-strategies": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz",
"integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==",
"dependencies": {
"workbox-core": "6.6.0"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",

View File

@ -51,8 +51,6 @@
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.84.1",
"webpack-cli": "5.1.1",
"workbox-routing": "6.6.0",
"workbox-strategies": "6.6.0",
"wrap-ansi": "8.1.0"
},
"devDependencies": {

View File

@ -236,44 +236,85 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// no scope required
if len(requiredScopeCategories) == 0 {
return
}
// Need OAuth2 token to be present.
scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ctx.Data["IsApiToken"] != true || !scopeExists {
return
}
ctx.Data["ApiTokenScopePublicRepoOnly"] = false
ctx.Data["ApiTokenScopePublicOrgOnly"] = false
// use the http method to determine the access level
requiredScopeLevel := auth_model.Read
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
requiredScopeLevel = auth_model.Write
}
// get the required scope for the given access level and category
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
return
}
// this context is used by the middleware in the specific route
ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository)
ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization)
allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
return
}
if allow {
return
}
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
}
}
// Contexter middleware already checks token for user sign in process.
func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APIContext) {
func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// If actions token is present
if true == ctx.Data["IsActionsToken"] {
return
}
// If OAuth2 token is present
if _, ok := ctx.Data["ApiTokenScope"]; ctx.Data["IsApiToken"] == true && ok {
// no scope required
if requiredScope == "" {
if true == ctx.Data["IsApiToken"] {
publicRepo, pubRepoExists := ctx.Data["ApiTokenScopePublicRepoOnly"]
publicOrg, pubOrgExists := ctx.Data["ApiTokenScopePublicOrgOnly"]
if pubRepoExists && publicRepo.(bool) &&
ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
return
}
// check scope
scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
allow, err := scope.HasScope(requiredScope)
if err != nil {
ctx.Error(http.StatusForbidden, "reqToken", "parsing token failed: "+err.Error())
return
}
if allow {
if pubOrgExists && publicOrg.(bool) &&
ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
// if requires 'repo' scope, but only has 'public_repo' scope, allow it only if the repo is public
if requiredScope == auth_model.AccessTokenScopeRepo {
if allowPublicRepo, err := scope.HasScope(auth_model.AccessTokenScopePublicRepo); err == nil && allowPublicRepo {
if ctx.Repo.Repository != nil && !ctx.Repo.Repository.IsPrivate {
return
}
}
}
ctx.Error(http.StatusForbidden, "reqToken", "token does not have required scope: "+requiredScope)
return
}
if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
@ -700,7 +741,7 @@ func Routes(ctx gocontext.Context) *web.Route {
ctx.Redirect(setting.AppSubURL + "/api/swagger")
})
}
m.Get("/version", misc.Version)
if setting.Federation.Enabled {
m.Get("/nodeinfo", misc.NodeInfo)
m.Group("/activitypub", func() {
@ -713,37 +754,43 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context_service.UserIDAssignmentAPI())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}
m.Get("/signing-key.gpg", misc.SigningKey)
m.Post("/markup", bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", misc.MarkdownRaw)
m.Get("/gitignore/templates", misc.ListGitignoresTemplates)
m.Get("/gitignore/templates/{name}", misc.GetGitignoreTemplateInfo)
m.Get("/licenses", misc.ListLicenseTemplates)
m.Get("/licenses/{name}", misc.GetLicenseTemplateInfo)
m.Get("/label/templates", misc.ListLabelTemplates)
m.Get("/label/templates/{name}", misc.GetLabelTemplate)
m.Group("/settings", func() {
m.Get("/ui", settings.GetGeneralUISettings)
m.Get("/api", settings.GetGeneralAPISettings)
m.Get("/attachment", settings.GetGeneralAttachmentSettings)
m.Get("/repository", settings.GetGeneralRepoSettings)
})
// Notifications (requires 'notification' scope)
// Misc (requires 'misc' scope)
m.Group("", func() {
m.Get("/version", misc.Version)
m.Get("/signing-key.gpg", misc.SigningKey)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
m.Get("/gitignore/templates", misc.ListGitignoresTemplates)
m.Get("/gitignore/templates/{name}", misc.GetGitignoreTemplateInfo)
m.Get("/licenses", misc.ListLicenseTemplates)
m.Get("/licenses/{name}", misc.GetLicenseTemplateInfo)
m.Get("/label/templates", misc.ListLabelTemplates)
m.Get("/label/templates/{name}", misc.GetLabelTemplate)
m.Group("/settings", func() {
m.Get("/ui", settings.GetGeneralUISettings)
m.Get("/api", settings.GetGeneralAPISettings)
m.Get("/attachment", settings.GetGeneralAttachmentSettings)
m.Get("/repository", settings.GetGeneralRepoSettings)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryMisc))
// Notifications (requires 'notifications' scope)
m.Group("/notifications", func() {
m.Combo("").
Get(notify.ListNotifications).
Put(notify.ReadNotifications)
Put(notify.ReadNotifications, reqToken())
m.Get("/new", notify.NewAvailable)
m.Combo("/threads/{id}").
Get(notify.GetThread).
Patch(notify.ReadThread)
}, reqToken(auth_model.AccessTokenScopeNotification))
Patch(notify.ReadThread, reqToken())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
// Users (no scope required)
// Users (requires user scope)
m.Group("/users", func() {
m.Get("/search", reqExploreSignIn(), user.Search)
@ -754,18 +801,18 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/heatmap", user.GetUserHeatmapData)
}
m.Get("/repos", reqExploreSignIn(), user.ListUserRepos)
m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos)
m.Group("/tokens", func() {
m.Combo("").Get(user.ListAccessTokens).
Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
m.Combo("/{id}").Delete(user.DeleteAccessToken)
Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken)
}, reqBasicAuth())
m.Get("/activities/feeds", user.ListUserActivityFeeds)
}, context_service.UserAssignmentAPI())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
// (no scope required)
// Users (requires user scope)
m.Group("/users", func() {
m.Group("/{username}", func() {
m.Get("/keys", user.ListPublicKeys)
@ -781,59 +828,61 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/subscriptions", user.GetWatchedRepos)
}, context_service.UserAssignmentAPI())
}, reqToken(""))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
m.Group("/user", func() {
m.Get("", user.GetAuthenticatedUser)
m.Group("/settings", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadUser), user.GetUserSettings)
m.Patch("", reqToken(auth_model.AccessTokenScopeUser), bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
})
m.Combo("/emails").Get(reqToken(auth_model.AccessTokenScopeReadUser), user.ListEmails).
Post(reqToken(auth_model.AccessTokenScopeUser), bind(api.CreateEmailOption{}), user.AddEmail).
Delete(reqToken(auth_model.AccessTokenScopeUser), bind(api.DeleteEmailOption{}), user.DeleteEmail)
m.Get("", user.GetUserSettings)
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
}, reqToken())
m.Combo("/emails").
Get(user.ListEmails).
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
m.Get("/followers", user.ListMyFollowers)
m.Group("/following", func() {
m.Get("", user.ListMyFollowing)
m.Group("/{username}", func() {
m.Get("", user.CheckMyFollowing)
m.Put("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Follow) // requires 'user:follow' scope
m.Delete("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Unfollow) // requires 'user:follow' scope
m.Put("", user.Follow)
m.Delete("", user.Unfollow)
}, context_service.UserAssignmentAPI())
})
// (admin:public_key scope)
m.Group("/keys", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.ListMyPublicKeys).
Post(reqToken(auth_model.AccessTokenScopeWritePublicKey), bind(api.CreateKeyOption{}), user.CreatePublicKey)
m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.GetPublicKey).
Delete(reqToken(auth_model.AccessTokenScopeWritePublicKey), user.DeletePublicKey)
m.Combo("").Get(user.ListMyPublicKeys).
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
m.Combo("/{id}").Get(user.GetPublicKey).
Delete(user.DeletePublicKey)
})
// (admin:application scope)
m.Group("/applications", func() {
m.Combo("/oauth2").
Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.ListOauth2Applications).
Post(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
Get(user.ListOauth2Applications).
Post(bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
m.Combo("/oauth2/{id}").
Delete(reqToken(auth_model.AccessTokenScopeWriteApplication), user.DeleteOauth2Application).
Patch(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.GetOauth2Application)
Delete(user.DeleteOauth2Application).
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
Get(user.GetOauth2Application)
})
// (admin:gpg_key scope)
m.Group("/gpg_keys", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.ListMyGPGKeys).
Post(reqToken(auth_model.AccessTokenScopeWriteGPGKey), bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetGPGKey).
Delete(reqToken(auth_model.AccessTokenScopeWriteGPGKey), user.DeleteGPGKey)
m.Combo("").Get(user.ListMyGPGKeys).
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
m.Combo("/{id}").Get(user.GetGPGKey).
Delete(user.DeleteGPGKey)
})
m.Get("/gpg_key_token", reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetVerificationToken)
m.Post("/gpg_key_verify", reqToken(auth_model.AccessTokenScopeReadGPGKey), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
m.Get("/gpg_key_token", user.GetVerificationToken)
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope)
m.Combo("/repos", reqToken(auth_model.AccessTokenScopeRepo)).Get(user.ListMyRepos).
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create)
// (repo scope)
@ -844,64 +893,65 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Put("", user.Star)
m.Delete("", user.Unstar)
}, repoAssignment())
}, reqToken(auth_model.AccessTokenScopeRepo))
m.Get("/times", reqToken(auth_model.AccessTokenScopeRepo), repo.ListMyTrackedTimes)
m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches)
m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos)
m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
m.Get("/subscriptions", user.GetMyWatchedRepos)
m.Get("/teams", org.ListUserTeams)
m.Group("/hooks", func() {
m.Combo("").Get(user.ListHooks).
Post(bind(api.CreateHookOption{}), user.CreateHook)
m.Combo("/{id}").Get(user.GetHook).
Patch(bind(api.EditHookOption{}), user.EditHook).
Delete(user.DeleteHook)
}, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled())
}, reqToken(""))
}, reqWebhooksEnabled())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories
m.Post("/org/{org}/repos", reqToken(auth_model.AccessTokenScopeAdminOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated)
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
reqToken(),
bind(api.CreateRepoOption{}),
repo.CreateOrgRepoDeprecated)
m.Combo("/repositories/{id}", reqToken(auth_model.AccessTokenScopeRepo)).Get(repo.GetByID)
// requires repo scope
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)
// Repos (requires repo scope)
m.Group("/repos", func() {
m.Get("/search", repo.Search)
m.Get("/issues/search", repo.SearchIssues)
// (repo scope)
m.Post("/migrate", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Group("/{username}/{reponame}", func() {
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
Delete(reqToken(auth_model.AccessTokenScopeDeleteRepo), reqOwner(), repo.Delete).
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
m.Post("/generate", reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
Delete(reqToken(), reqOwner(), repo.Delete).
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
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("/accept", repo.AcceptTransfer)
m.Post("/reject", repo.RejectTransfer)
}, reqToken(auth_model.AccessTokenScopeRepo))
m.Combo("/notifications", reqToken(auth_model.AccessTokenScopeNotification)).
Get(notify.ListRepoNotifications).
Put(notify.ReadRepoNotifications)
}, reqToken())
m.Group("/hooks/git", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListGitHooks)
m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetGitHook).
Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditGitHookOption{}), repo.EditGitHook).
Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteGitHook)
m.Combo("").Get(repo.GetGitHook).
Patch(bind(api.EditGitHookOption{}), repo.EditGitHook).
Delete(repo.DeleteGitHook)
})
}, reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
}, reqToken(), reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
m.Group("/hooks", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListHooks).
Post(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.CreateHookOption{}), repo.CreateHook)
m.Combo("").Get(repo.ListHooks).
Post(bind(api.CreateHookOption{}), repo.CreateHook)
m.Group("/{id}", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetHook).
Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditHookOption{}), repo.EditHook).
Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteHook)
m.Post("/tests", reqToken(auth_model.AccessTokenScopeReadRepoHook), context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
m.Combo("").Get(repo.GetHook).
Patch(bind(api.EditHookOption{}), repo.EditHook).
Delete(repo.DeleteHook)
m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
})
}, reqAdmin(), reqWebhooksEnabled())
}, reqToken(), reqAdmin(), reqWebhooksEnabled())
m.Group("/collaborators", func() {
m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
m.Group("/{collaborator}", func() {
@ -910,25 +960,25 @@ func Routes(ctx gocontext.Context) *web.Route {
Delete(reqAdmin(), repo.DeleteCollaborator)
m.Get("/permission", repo.GetRepoPermissions)
})
}, reqToken(auth_model.AccessTokenScopeRepo))
m.Get("/assignees", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/reviewers", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetReviewers)
}, reqToken())
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
m.Group("/teams", func() {
m.Get("", reqAnyRepoReader(), repo.ListTeams)
m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam).
Put(reqAdmin(), repo.AddTeam).
Delete(reqAdmin(), repo.DeleteTeam)
}, reqToken(auth_model.AccessTokenScopeRepo))
}, reqToken())
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Group("/branches", func() {
m.Get("", repo.ListBranches)
m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
@ -938,218 +988,112 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection)
})
}, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
}, reqToken(), reqAdmin())
m.Group("/tags", func() {
m.Get("", repo.ListTags)
m.Get("/*", repo.GetTag)
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTag)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(), repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
m.Combo("/{id}").Get(repo.GetDeployKey).
Delete(repo.DeleteDeploykey)
}, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
}, reqToken(), reqAdmin())
m.Group("/times", func() {
m.Combo("").Get(repo.ListTrackedTimesByRepository)
m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser)
}, mustEnableIssues, reqToken(auth_model.AccessTokenScopeRepo))
}, mustEnableIssues, reqToken())
m.Group("/wiki", func() {
m.Combo("/page/{pageName}").
Get(repo.GetWikiPage).
Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
Delete(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
m.Get("/revisions/{pageName}", repo.ListPageRevisions)
m.Post("/new", mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki)
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Get("/pinned", repo.ListPinnedIssues)
m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() {
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueCommentAttachment)
m.Combo("/{asset}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
}, mustEnableAttachments)
})
})
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
m.Combo("/{id}", reqToken(auth_model.AccessTokenScopeRepo)).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
Put(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueLabel)
})
m.Group("/times", func() {
m.Combo("").
Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}), repo.AddTime).
Delete(repo.ResetIssueTime)
m.Delete("/{id}", repo.DeleteTime)
}, reqToken(auth_model.AccessTokenScopeRepo))
m.Combo("/deadline").Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() {
m.Post("/start", reqToken(auth_model.AccessTokenScopeRepo), repo.StartIssueStopwatch)
m.Post("/stop", reqToken(auth_model.AccessTokenScopeRepo), repo.StopIssueStopwatch)
m.Delete("/delete", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueStopwatch)
})
m.Group("/subscriptions", func() {
m.Get("", repo.GetIssueSubscribers)
m.Get("/check", reqToken(auth_model.AccessTokenScopeRepo), repo.CheckIssueSubscription)
m.Put("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.AddIssueSubscription)
m.Delete("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.DelIssueSubscription)
})
m.Combo("/reactions").
Get(repo.GetIssueReactions).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueReaction).
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueAttachments).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueAttachment)
m.Combo("/{asset}").
Get(repo.GetIssueAttachment).
Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
}, mustEnableAttachments)
m.Combo("/dependencies").
Get(repo.GetIssueDependencies).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
m.Combo("/blocks").
Get(repo.GetIssueBlocks).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
m.Group("/pin", func() {
m.Combo("").
Post(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.PinIssue).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.UnpinIssue)
m.Patch("/{position}", reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.MoveIssuePin)
})
})
}, mustEnableIssuesOrPulls)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListLabels).
Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
m.Combo("/{id}").Get(repo.GetLabel).
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
})
m.Post("/markup", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw)
m.Group("/milestones", func() {
m.Combo("").Get(repo.ListMilestones).
Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
m.Combo("/{id}").Get(repo.GetMilestone).
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
m.Get("/stargazers", repo.ListStargazers)
m.Get("/subscribers", repo.ListSubscribers)
m.Group("/subscription", func() {
m.Get("", user.IsWatching)
m.Put("", reqToken(auth_model.AccessTokenScopeRepo), user.Watch)
m.Delete("", reqToken(auth_model.AccessTokenScopeRepo), user.Unwatch)
m.Put("", reqToken(), user.Watch)
m.Delete("", reqToken(), user.Unwatch)
})
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
m.Combo("/latest").Get(repo.GetLatestRelease)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetRelease).
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
m.Group("/assets", func() {
m.Combo("").Get(repo.ListReleaseAttachments).
Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
m.Combo("/{asset}").Get(repo.GetReleaseAttachment).
Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
})
})
m.Group("/tags", func() {
m.Combo("/{tag}").
Get(repo.GetReleaseByTag).
Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
})
}, reqRepoReader(unit.TypeReleases))
m.Post("/mirror-sync", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo), repo.PushMirrorSync)
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), repo.PushMirrorSync)
m.Group("/push_mirrors", func() {
m.Combo("").Get(repo.ListPushMirrors).
Post(bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
m.Combo("/{name}").
Delete(repo.DeletePushMirrorByRemoteName).
Get(repo.GetPushMirrorByName)
}, reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo))
}, reqAdmin(), reqToken())
m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
m.Group("/pulls", func() {
m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
m.Get("/pinned", repo.ListPinnedPullRequests)
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
m.Post("/update", reqToken(auth_model.AccessTokenScopeRepo), repo.UpdatePullRequest)
m.Post("/update", reqToken(), repo.UpdatePullRequest)
m.Get("/commits", repo.GetPullRequestCommits)
m.Get("/files", repo.GetPullRequestFiles)
m.Combo("/merge").Get(repo.IsPullRequestMerged).
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CancelScheduledAutoMerge)
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
m.Group("/reviews", func() {
m.Combo("").
Get(repo.ListPullReviews).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
m.Group("/{id}", func() {
m.Combo("").
Get(repo.GetPullReview).
Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeletePullReview).
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
Delete(reqToken(), repo.DeletePullReview).
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments").
Get(repo.GetPullReviewComments)
m.Post("/dismissals", reqToken(auth_model.AccessTokenScopeRepo), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
m.Post("/undismissals", reqToken(auth_model.AccessTokenScopeRepo), repo.UnDismissPullReview)
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
})
})
m.Combo("/requested_reviewers", reqToken(auth_model.AccessTokenScopeRepo)).
m.Combo("/requested_reviewers", reqToken()).
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
})
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() {
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
Post(reqToken(auth_model.AccessTokenScopeRepoStatus), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(unit.TypeCode))
m.Group("/commits", func() {
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
@ -1170,24 +1114,24 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote)
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
m.Group("/contents", func() {
m.Get("", repo.GetContentsList)
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
m.Get("/*", repo.GetContents)
m.Group("/*", func() {
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, repo.UpdateFile)
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, repo.DeleteFile)
}, reqToken(auth_model.AccessTokenScopeRepo))
}, reqToken())
}, reqRepoReader(unit.TypeCode))
m.Get("/signing-key.gpg", misc.SigningKey)
m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics).
Put(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
m.Group("/{topic}", func() {
m.Combo("").Put(reqToken(auth_model.AccessTokenScopeRepo), repo.AddTopic).
Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTopic)
m.Combo("").Put(reqToken(), repo.AddTopic).
Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin())
}, reqAnyRepoReader())
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
@ -1197,54 +1141,177 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
}, repoAssignment())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
// Notifications (requires notifications scope)
m.Group("/repos", func() {
m.Group("/{username}/{reponame}", func() {
m.Combo("/notifications", reqToken()).
Get(notify.ListRepoNotifications).
Put(notify.ReadRepoNotifications)
}, repoAssignment())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
// Issue (requires issue scope)
m.Group("/repos", func() {
m.Get("/issues/search", repo.SearchIssues)
m.Group("/{username}/{reponame}", func() {
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Get("/pinned", repo.ListPinnedIssues)
m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() {
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived, reqToken(), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
Delete(reqToken(), repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment)
m.Combo("/{asset}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
}, mustEnableAttachments)
})
})
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
Delete(reqToken(), repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
})
m.Group("/times", func() {
m.Combo("").
Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}), repo.AddTime).
Delete(repo.ResetIssueTime)
m.Delete("/{id}", repo.DeleteTime)
}, reqToken())
m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() {
m.Post("/start", repo.StartIssueStopwatch)
m.Post("/stop", repo.StopIssueStopwatch)
m.Delete("/delete", repo.DeleteIssueStopwatch)
}, reqToken())
m.Group("/subscriptions", func() {
m.Get("", repo.GetIssueSubscribers)
m.Get("/check", reqToken(), repo.CheckIssueSubscription)
m.Put("/{user}", reqToken(), repo.AddIssueSubscription)
m.Delete("/{user}", reqToken(), repo.DelIssueSubscription)
})
m.Combo("/reactions").
Get(repo.GetIssueReactions).
Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueReaction).
Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
m.Group("/assets", func() {
m.Combo("").
Get(repo.ListIssueAttachments).
Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
m.Combo("/{asset}").
Get(repo.GetIssueAttachment).
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueAttachment)
}, mustEnableAttachments)
m.Combo("/dependencies").
Get(repo.GetIssueDependencies).
Post(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
Delete(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
m.Combo("/blocks").
Get(repo.GetIssueBlocks).
Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
Delete(reqToken(), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
m.Group("/pin", func() {
m.Combo("").
Post(reqToken(), reqAdmin(), repo.PinIssue).
Delete(reqToken(), reqAdmin(), repo.UnpinIssue)
m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin)
})
})
}, mustEnableIssuesOrPulls)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListLabels).
Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
m.Combo("/{id}").Get(repo.GetLabel).
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
})
m.Group("/milestones", func() {
m.Combo("").Get(repo.ListMilestones).
Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
m.Combo("/{id}").Get(repo.GetMilestone).
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
}, repoAssignment())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}/{version}", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadPackage), packages.GetPackage)
m.Delete("", reqToken(auth_model.AccessTokenScopeDeletePackage), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Get("/files", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackageFiles)
m.Get("", reqToken(), packages.GetPackage)
m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Get("/files", reqToken(), packages.ListPackageFiles)
})
m.Get("/", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackages)
}, context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
// Organizations
m.Get("/user/orgs", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMyOrgs)
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetUserOrgsPermissions)
}, context_service.UserAssignmentAPI())
m.Post("/orgs", reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll)
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Group("/orgs/{org}", func() {
m.Combo("").Get(org.Get).
Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.Delete)
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Group("/members", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMembers)
m.Combo("/{username}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.IsMember).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteMember)
m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
})
m.Group("/public_members", func() {
m.Get("", org.ListPublicMembers)
m.Combo("/{username}").Get(org.IsPublicMember).
Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.PublicizeMember).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.ConcealMember)
Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
})
m.Group("/teams", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListTeams)
m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", reqToken(auth_model.AccessTokenScopeReadOrg), org.SearchTeam)
m.Get("", reqToken(), org.ListTeams)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", reqToken(), org.SearchTeam)
}, reqOrgMembership())
m.Group("/labels", func() {
m.Get("", org.ListLabels)
m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetLabel).
Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteLabel)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
m.Combo("/{id}").Get(reqToken(), org.GetLabel).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
})
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
@ -1252,29 +1319,29 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Combo("/{id}").Get(org.GetHook).
Patch(bind(api.EditHookOption{}), org.EditHook).
Delete(org.DeleteHook)
}, reqToken(auth_model.AccessTokenScopeAdminOrgHook), reqOrgOwnership(), reqWebhooksEnabled())
}, reqToken(), reqOrgOwnership(), reqWebhooksEnabled())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
}, orgAssignment(true))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeam).
Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteTeam)
m.Combo("").Get(reqToken(), org.GetTeam).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam)
m.Group("/members", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMembers)
m.Get("", reqToken(), org.GetTeamMembers)
m.Combo("/{username}").
Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMember).
Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.AddTeamMember).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.RemoveTeamMember)
Get(reqToken(), org.GetTeamMember).
Put(reqToken(), reqOrgOwnership(), org.AddTeamMember).
Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember)
})
m.Group("/repos", func() {
m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepos)
m.Get("", reqToken(), org.GetTeamRepos)
m.Combo("/{org}/{reponame}").
Put(reqToken(auth_model.AccessTokenScopeWriteOrg), org.AddTeamRepository).
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), org.RemoveTeamRepository).
Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepo)
Put(reqToken(), org.AddTeamRepository).
Delete(reqToken(), org.RemoveTeamRepository).
Get(reqToken(), org.GetTeamRepo)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, orgAssignment(false, true), reqToken(""), reqTeamMembership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())
m.Group("/admin", func() {
m.Group("/cron", func() {
@ -1314,11 +1381,11 @@ func Routes(ctx gocontext.Context) *web.Route {
Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook)
})
}, reqToken(auth_model.AccessTokenScopeSudo), reqSiteAdmin())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
m.Group("/topics", func() {
m.Get("/search", repo.TopicSearch)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
}, sudo())
return m

View File

@ -35,7 +35,6 @@ import (
"code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/session"
"gopkg.in/ini.v1"
)
const (
@ -371,17 +370,11 @@ func SubmitInstall(ctx *context.Context) {
}
// Save settings.
cfg := ini.Empty()
isFile, err := util.IsFile(setting.CustomConf)
cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true})
if err != nil {
log.Error("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
}
if isFile {
// Keeps custom settings if there is already something.
if err = cfg.Append(setting.CustomConf); err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String())
cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)

View File

@ -8,11 +8,13 @@ import (
"fmt"
"net/http"
"runtime"
"sort"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/web"
@ -26,6 +28,7 @@ const (
tplQueue base.TplName = "admin/queue"
tplStacktrace base.TplName = "admin/stacktrace"
tplQueueManage base.TplName = "admin/queue_manage"
tplStats base.TplName = "admin/stats"
)
var sysStatus struct {
@ -111,7 +114,6 @@ func updateSystemStatus() {
func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = activities_model.GetStatistic()
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate()
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion()
// FIXME: update periodically
@ -126,7 +128,6 @@ func DashboardPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = activities_model.GetStatistic()
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
@ -153,3 +154,30 @@ func CronTasks(ctx *context.Context) {
ctx.Data["Entries"] = cron.ListTasks()
ctx.HTML(http.StatusOK, tplCron)
}
func MonitorStats(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.stats")
ctx.Data["PageIsAdminMonitorStats"] = true
bs, err := json.Marshal(activities_model.GetStatistic().Counter)
if err != nil {
ctx.ServerError("MonitorStats", err)
return
}
statsCounter := map[string]any{}
err = json.Unmarshal(bs, &statsCounter)
if err != nil {
ctx.ServerError("MonitorStats", err)
return
}
statsKeys := make([]string, 0, len(statsCounter))
for k := range statsCounter {
if statsCounter[k] == nil {
continue
}
statsKeys = append(statsKeys, k)
}
sort.Strings(statsKeys)
ctx.Data["StatsKeys"] = statsKeys
ctx.Data["StatsCounter"] = statsCounter
ctx.HTML(http.StatusOK, tplStats)
}

View File

@ -695,7 +695,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
}
// "The authorization server MUST ... require client authentication for confidential clients"
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
errorDescription := "invalid client secret"
if form.ClientSecret == "" {
errorDescription = "invalid empty client secret"
@ -753,7 +753,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
})
return
}
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
errorDescription := "invalid client secret"
if form.ClientSecret == "" {
errorDescription = "invalid empty client secret"

View File

@ -26,10 +26,9 @@ import (
)
const (
tplProjects base.TplName = "org/projects/list"
tplProjectsNew base.TplName = "org/projects/new"
tplProjectsView base.TplName = "org/projects/view"
tplGenericProjectsNew base.TplName = "user/project"
tplProjects base.TplName = "org/projects/list"
tplProjectsNew base.TplName = "org/projects/new"
tplProjectsView base.TplName = "org/projects/view"
)
// MustEnableProjects check if projects are enabled in settings
@ -125,14 +124,15 @@ func canWriteProjects(ctx *context.Context) bool {
return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
}
// NewProject render creating a project page
func NewProject(ctx *context.Context) {
// RenderNewProject render creating a project page
func RenderNewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["PageIsViewProjects"] = true
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects"
shared_user.RenderUserHeader(ctx)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -144,11 +144,7 @@ func NewProjectPost(ctx *context.Context) {
shared_user.RenderUserHeader(ctx)
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["PageIsViewProjects"] = true
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.HTML(http.StatusOK, tplProjectsNew)
RenderNewProject(ctx)
return
}
@ -227,8 +223,8 @@ func DeleteProject(ctx *context.Context) {
})
}
// EditProject allows a project to be edited
func EditProject(ctx *context.Context) {
// RenderEditProject allows a project to be edited
func RenderEditProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
ctx.Data["PageIsViewProjects"] = true
@ -257,6 +253,7 @@ func EditProject(ctx *context.Context) {
ctx.Data["redirect"] = ctx.FormString("redirect")
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
ctx.Data["card_type"] = p.CardType
ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -264,11 +261,13 @@ func EditProject(ctx *context.Context) {
// EditProjectPost response for editing a project
func EditProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
projectID := ctx.ParamsInt64(":id")
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
ctx.Data["PageIsViewProjects"] = true
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
shared_user.RenderUserHeader(ctx)
@ -277,7 +276,7 @@ func EditProjectPost(ctx *context.Context) {
return
}
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)

View File

@ -110,11 +110,6 @@ func ServeAttachment(ctx *context.Context, uuid string) {
return
}
} else { // If we have the repository we check access
context.CheckRepoScopedToken(ctx, repository)
if ctx.Written() {
return
}
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())

View File

@ -152,7 +152,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
return
}
context.CheckRepoScopedToken(ctx, repo)
context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode))
if ctx.Written() {
return
}

View File

@ -27,10 +27,9 @@ import (
)
const (
tplProjects base.TplName = "repo/projects/list"
tplProjectsNew base.TplName = "repo/projects/new"
tplProjectsView base.TplName = "repo/projects/view"
tplGenericProjectsNew base.TplName = "user/project"
tplProjects base.TplName = "repo/projects/list"
tplProjectsNew base.TplName = "repo/projects/new"
tplProjectsView base.TplName = "repo/projects/view"
)
// MustEnableProjects check if projects are enabled in settings
@ -121,12 +120,13 @@ func Projects(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplProjects)
}
// NewProject render creating a project page
func NewProject(ctx *context.Context) {
// RenderNewProject render creating a project page
func RenderNewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -136,10 +136,7 @@ func NewProjectPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.HTML(http.StatusOK, tplProjectsNew)
RenderNewProject(ctx)
return
}
@ -211,8 +208,8 @@ func DeleteProject(ctx *context.Context) {
})
}
// EditProject allows a project to be edited
func EditProject(ctx *context.Context) {
// RenderEditProject allows a project to be edited
func RenderEditProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
@ -237,6 +234,7 @@ func EditProject(ctx *context.Context) {
ctx.Data["content"] = p.Description
ctx.Data["card_type"] = p.CardType
ctx.Data["redirect"] = ctx.FormString("redirect")
ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -244,17 +242,20 @@ func EditProject(ctx *context.Context) {
// EditProjectPost response for editing a project
func EditProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
projectID := ctx.ParamsInt64(":id")
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplProjectsNew)
return
}
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)

View File

@ -89,6 +89,7 @@ func DeleteApplication(ctx *context.Context) {
}
func loadApplicationsData(ctx *context.Context) {
ctx.Data["AccessTokenScopePublicOnly"] = auth_model.AccessTokenScopePublicOnly
tokens, err := auth_model.ListAccessTokens(auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
@ -96,6 +97,7 @@ func loadApplicationsData(ctx *context.Context) {
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
if setting.OAuth2.Enable {
ctx.Data["Applications"], err = auth_model.GetOAuth2ApplicationsByUserID(ctx, ctx.Doer.ID)
if err != nil {

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
@ -40,7 +41,7 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
// TODO validate redirect URI
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient,
})
@ -93,7 +94,7 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
if ctx.Data["App"], err = auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{
ID: ctx.ParamsInt64("id"),
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient,
}); err != nil {

View File

@ -538,8 +538,8 @@ func registerRoutes(m *web.Route) {
// ***** START: Admin *****
m.Group("/admin", func() {
m.Get("", adminReq, admin.Dashboard)
m.Post("", adminReq, web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
m.Get("", admin.Dashboard)
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
m.Group("/config", func() {
m.Get("", admin.Config)
@ -548,6 +548,7 @@ func registerRoutes(m *web.Route) {
})
m.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
m.Get("/stacktrace", admin.Stacktrace)
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
@ -824,13 +825,13 @@ func registerRoutes(m *web.Route) {
m.Get("/{id}", org.ViewProject)
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead))
m.Group("", func() { //nolint:dupl
m.Get("/new", org.NewProject)
m.Get("/new", org.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
m.Post("/delete", org.DeleteProject)
m.Get("/edit", org.EditProject)
m.Get("/edit", org.RenderEditProject)
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
m.Post("/{action:open|close}", org.ChangeProjectStatus)
@ -1159,13 +1160,13 @@ func registerRoutes(m *web.Route) {
m.Get("", repo.Projects)
m.Get("/{id}", repo.ViewProject)
m.Group("", func() { //nolint:dupl
m.Get("/new", repo.NewProject)
m.Get("/new", repo.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
m.Post("/delete", repo.DeleteProject)
m.Get("/edit", repo.EditProject)
m.Get("/edit", repo.RenderEditProject)
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
m.Post("/{action:open|close}", repo.ChangeProjectStatus)

View File

@ -398,7 +398,7 @@ func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
// EditOAuth2ApplicationForm form for editing oauth2 applications
type EditOAuth2ApplicationForm struct {
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
RedirectURI string `binding:"Required" form:"redirect_uri"`
RedirectURIs string `binding:"Required" form:"redirect_uris"`
ConfidentialClient bool `form:"confidential_client"`
}

View File

@ -112,12 +112,12 @@ func TestNewAccessTokenForm_GetScope(t *testing.T) {
expectedErr error
}{
{
form: NewAccessTokenForm{Name: "test", Scope: []string{"repo"}},
scope: "repo",
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}},
scope: "read:repository",
},
{
form: NewAccessTokenForm{Name: "test", Scope: []string{"repo", "user"}},
scope: "repo,user",
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}},
scope: "read:repository,write:user",
},
}

View File

@ -73,7 +73,7 @@ func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewe
}
if comment != nil {
notification.NotifyPullReviewRequest(ctx, doer, issue, reviewer, isAdd, comment)
notification.NotifyPullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment)
}
return comment, err
@ -260,7 +260,7 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
continue
}
comment.AssigneeID = member.ID
notification.NotifyPullReviewRequest(ctx, doer, issue, member, isAdd, comment)
notification.NotifyPullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
}
return comment, err

View File

@ -8,6 +8,7 @@ import (
"strconv"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
@ -58,7 +59,7 @@ func GetListLockHandler(ctx *context.Context) {
}
repository.MustOwner(ctx)
context.CheckRepoScopedToken(ctx, repository)
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
if ctx.Written() {
return
}
@ -150,7 +151,7 @@ func PostLockHandler(ctx *context.Context) {
}
repository.MustOwner(ctx)
context.CheckRepoScopedToken(ctx, repository)
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
if ctx.Written() {
return
}
@ -222,7 +223,7 @@ func VerifyLockHandler(ctx *context.Context) {
}
repository.MustOwner(ctx)
context.CheckRepoScopedToken(ctx, repository)
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
if ctx.Written() {
return
}
@ -293,7 +294,7 @@ func UnLockHandler(ctx *context.Context) {
}
repository.MustOwner(ctx)
context.CheckRepoScopedToken(ctx, repository)
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
if ctx.Written() {
return
}

View File

@ -18,6 +18,7 @@ import (
"strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@ -423,7 +424,12 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
return nil
}
context.CheckRepoScopedToken(ctx, repository)
if requireWrite {
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
} else {
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
}
if ctx.Written() {
return nil
}

View File

@ -413,7 +413,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
milestone = issue.Milestone.Title
}
var reactions []*base.Reaction
var reactions []*gitlab.AwardEmoji
awardPage := 1
for {
awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
@ -421,9 +421,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
}
for i := range awards {
reactions = append(reactions, g.awardToReaction(awards[i]))
}
reactions = append(reactions, awards...)
if len(awards) < perPage {
break
@ -442,7 +440,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
State: issue.State,
Created: *issue.CreatedAt,
Labels: labels,
Reactions: reactions,
Reactions: g.awardsToReactions(reactions),
Closed: issue.ClosedAt,
IsLocked: issue.DiscussionLocked,
Updated: *issue.UpdatedAt,
@ -577,7 +575,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
milestone = pr.Milestone.Title
}
var reactions []*base.Reaction
var reactions []*gitlab.AwardEmoji
awardPage := 1
for {
awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
@ -585,9 +583,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
}
for i := range awards {
reactions = append(reactions, g.awardToReaction(awards[i]))
}
reactions = append(reactions, awards...)
if len(awards) < perPage {
break
@ -614,7 +610,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
MergeCommitSHA: pr.MergeCommitSHA,
MergedTime: mergeTime,
IsLocked: locked,
Reactions: reactions,
Reactions: g.awardsToReactions(reactions),
Head: base.PullRequestBranch{
Ref: pr.SourceBranch,
SHA: pr.SHA,
@ -675,10 +671,19 @@ func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie
return reviews, nil
}
func (g *GitlabDownloader) awardToReaction(award *gitlab.AwardEmoji) *base.Reaction {
return &base.Reaction{
UserID: int64(award.User.ID),
UserName: award.User.Username,
Content: award.Name,
func (g *GitlabDownloader) awardsToReactions(awards []*gitlab.AwardEmoji) []*base.Reaction {
result := make([]*base.Reaction, 0, len(awards))
uniqCheck := make(map[string]struct{})
for _, award := range awards {
uid := fmt.Sprintf("%s%d", award.Name, award.User.ID)
if _, ok := uniqCheck[uid]; !ok {
result = append(result, &base.Reaction{
UserID: int64(award.User.ID),
UserName: award.User.Username,
Content: award.Name,
})
uniqCheck[uid] = struct{}{}
}
}
return result
}

View File

@ -13,6 +13,7 @@ import (
"testing"
"time"
"code.gitea.io/gitea/modules/json"
base "code.gitea.io/gitea/modules/migration"
"github.com/stretchr/testify/assert"
@ -469,3 +470,49 @@ func TestGitlabGetReviews(t *testing.T) {
assertReviewsEqual(t, []*base.Review{&review}, rvs)
}
}
func TestAwardsToReactions(t *testing.T) {
downloader := &GitlabDownloader{}
// yes gitlab can have duplicated reactions (https://gitlab.com/jaywink/socialhome/-/issues/24)
testResponse := `
[
{
"name": "thumbsup",
"user": {
"id": 1241334,
"username": "lafriks"
}
},
{
"name": "thumbsup",
"user": {
"id": 1241334,
"username": "lafriks"
}
},
{
"name": "thumbsup",
"user": {
"id": 4575606,
"username": "real6543"
}
}
]
`
var awards []*gitlab.AwardEmoji
assert.NoError(t, json.Unmarshal([]byte(testResponse), &awards))
reactions := downloader.awardsToReactions(awards)
assert.EqualValues(t, []*base.Reaction{
{
UserName: "lafriks",
UserID: 1241334,
Content: "thumbsup",
},
{
UserName: "real6543",
UserID: 4575606,
Content: "thumbsup",
},
}, reactions)
}

View File

@ -115,7 +115,7 @@ func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Du
return nil
}
func gatherMissingRepoRecords(ctx context.Context) ([]*repo_model.Repository, error) {
func gatherMissingRepoRecords(ctx context.Context) (repo_model.RepositoryList, error) {
repos := make([]*repo_model.Repository, 0, 10)
if err := db.Iterate(
ctx,

View File

@ -50,7 +50,7 @@ type ChangeRepoFile struct {
Options *RepoFileOptions
}
// UpdateRepoFilesOptions holds the repository files update options
// ChangeRepoFilesOptions holds the repository files update options
type ChangeRepoFilesOptions struct {
LastCommitID string
OldBranch string
@ -159,7 +159,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err
}
treePaths := []string{}
var treePaths []string
for _, file := range opts.Files {
// If FromTreePath is not set, set it to the opts.TreePath
if file.TreePath != "" && file.FromTreePath == "" {
@ -302,7 +302,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err
}
default:
return nil, fmt.Errorf("Invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath)
}
}
@ -334,16 +334,16 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err
}
filesReponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths)
filesResponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths)
if err != nil {
return nil, err
}
if repo.IsEmpty {
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty")
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch")
}
return filesReponse, nil
return filesResponse, nil
}
// handles the check for various issues for ChangeRepoFiles
@ -437,7 +437,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
return nil
}
// handle creating or updating a file for ChangeRepoFiles
// CreateOrUpdateFile handles creating or updating a file for ChangeRepoFiles
func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
// Get the two paths (might be the same if not moving) from the index if they exist
filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath)
@ -540,7 +540,7 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
if !exist {
if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(file.Content)); err != nil {
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
}
return err
}

View File

@ -719,9 +719,9 @@ func (m *webhookNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue
}
}
func (m *webhookNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
if !issue.IsPull {
log.Warn("NotifyPullReviewRequest: issue is not a pull request: %v", issue.ID)
log.Warn("NotifyPullRequestReviewRequest: issue is not a pull request: %v", issue.ID)
return
}
mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests)

View File

@ -133,7 +133,7 @@
</div>
<div class="field">
<label>{{.locale.Tr "admin.auths.map_group_to_team"}}</label>
<input name="group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
<textarea name="group_team_map" rows="5" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>{{$cfg.GroupTeamMap}}</textarea>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.map_group_to_team_removal"}}</label>
@ -360,7 +360,7 @@
</div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
<textarea name="oauth2_group_team_map" rows="5" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>{{$cfg.GroupTeamMap}}</textarea>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>

View File

@ -106,7 +106,7 @@
</div>
<div class="field">
<label>{{.locale.Tr "admin.auths.map_group_to_team"}}</label>
<input name="group_team_map" value="{{.group_team_map}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
<textarea name="group_team_map" rows="5" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>{{.group_team_map}}</textarea>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.map_group_to_team_removal"}}</label>

View File

@ -100,7 +100,7 @@
</div>
<div class="field">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team"}}</label>
<input name="oauth2_group_team_map" value="{{.oauth2_group_team_map}}" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
<textarea name="oauth2_group_team_map" rows="5" placeholder='e.g. {"Developer": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>{{.oauth2_group_team_map}}</textarea>
</div>
<div class="ui checkbox">
<label>{{.locale.Tr "admin.auths.oauth2_map_group_to_team_removal"}}</label>

View File

@ -5,14 +5,6 @@
<p>{{(.locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p>
</div>
{{end}}
<h4 class="ui top attached header">
{{.locale.Tr "admin.dashboard.statistic"}}
</h4>
<div class="ui attached segment">
<p>
{{.locale.Tr "admin.dashboard.statistic_info" .Stats.Counter.User .Stats.Counter.Org .Stats.Counter.PublicKey .Stats.Counter.Repo .Stats.Counter.Watch .Stats.Counter.Star .Stats.Counter.Action .Stats.Counter.Access .Stats.Counter.Issue .Stats.Counter.Comment .Stats.Counter.Oauth .Stats.Counter.Follow .Stats.Counter.Mirror .Stats.Counter.Release .Stats.Counter.AuthSource .Stats.Counter.Webhook .Stats.Counter.Milestone .Stats.Counter.Label .Stats.Counter.HookTask .Stats.Counter.Team .Stats.Counter.UpdateTask .Stats.Counter.Attachment | Str2html}}
</p>
</div>
<h4 class="ui top attached header">
{{.locale.Tr "admin.dashboard.operations"}}
</h4>

View File

@ -53,6 +53,9 @@
<div class="item">
{{.locale.Tr "admin.monitor"}}
<div class="menu">
<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/admin/monitor/stats">
{{.locale.Tr "admin.monitor.stats"}}
</a>
<a class="{{if .PageIsAdminMonitorCron}}active {{end}}item" href="{{AppSubUrl}}/admin/monitor/cron">
{{.locale.Tr "admin.monitor.cron"}}
</a>

View File

@ -0,0 +1,17 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{.locale.Tr "admin.dashboard.statistic"}}
</h4>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
{{range $statsKey := .StatsKeys}}
<tr>
<td width="200">{{$statsKey}}</td>
<td>{{index $.StatsCounter $statsKey}}</td>
</tr>
{{end}}
</table>
</div>
</div>
{{template "admin/layout_footer" .}}

View File

@ -12,7 +12,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
assetUrlPrefix: '{{AssetUrlPrefix}}',
runModeIsProd: {{.RunModeIsProd}},
customEmojis: {{CustomEmojis}},
useServiceWorker: {{UseServiceWorker}},
csrfToken: '{{.CsrfToken}}',
pageData: {{.PageData}},
notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}}

View File

@ -1,5 +1,5 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
<div role="main" aria-label="{{.Title}}" class="page-content organization projects edit-project new">
{{template "user/overview/header" .}}
{{template "projects/new" .}}
</div>

View File

@ -1,87 +1,70 @@
<div role="main" aria-label="{{.Title}}" class="page-content repository projects edit-project new milestone">
<div class="ui container">
<div class="navbar">
{{if and .CanWriteProjects .PageIsEditProject}}
<div class="ui right floated secondary menu">
<a class="ui small green button" href="{{$.HomeLink}}/-/projects/new">{{.locale.Tr "repo.milestones.new"}}</a>
</div>
<div class="ui container">
<h2 class="ui dividing header">
{{if .PageIsEditProjects}}
{{.locale.Tr "repo.projects.edit"}}
<div class="sub header">{{.locale.Tr "repo.projects.edit_subheader"}}</div>
{{else}}
{{.locale.Tr "repo.projects.new"}}
<div class="sub header">{{.locale.Tr "repo.projects.new_subheader"}}</div>
{{end}}
</div>
<div class="ui divider"></div>
<h2 class="ui dividing header">
{{if .PageIsEditProjects}}
{{.locale.Tr "repo.projects.edit"}}
<div class="sub header">{{.locale.Tr "repo.projects.edit_subheader"}}</div>
{{else}}
{{.locale.Tr "repo.projects.new"}}
<div class="sub header">{{.locale.Tr "repo.projects.new_subheader"}}</div>
{{end}}
</h2>
{{template "base/alert" .}}
<form class="ui form grid" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="eleven wide column">
<input type="hidden" id="redirect" name="redirect" value="{{.redirect}}">
<div class="field {{if .Err_Title}}error{{end}}">
<label>{{.locale.Tr "repo.projects.title"}}</label>
<input name="title" placeholder="{{.locale.Tr "repo.projects.title"}}" value="{{.title}}" autofocus required>
</div>
<div class="field">
<label>{{.locale.Tr "repo.projects.description"}}</label>
<textarea name="content" placeholder="{{.locale.Tr "repo.projects.description_placeholder"}}">{{.content}}</textarea>
</div>
{{if not .PageIsEditProjects}}
<div class="field">
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="board_type" value="{{.type}}">
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
<div class="menu">
{{range $element := .BoardTypes}}
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
{{end}}
</h2>
{{template "base/alert" .}}
<form class="ui form grid" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="eleven wide column">
<input type="hidden" id="redirect" name="redirect" value="{{.redirect}}">
<div class="field {{if .Err_Title}}error{{end}}">
<label>{{.locale.Tr "repo.projects.title"}}</label>
<input name="title" placeholder="{{.locale.Tr "repo.projects.title"}}" value="{{.title}}" autofocus required>
</div>
<div class="field">
<label>{{.locale.Tr "repo.projects.description"}}</label>
<textarea name="content" placeholder="{{.locale.Tr "repo.projects.description_placeholder"}}">{{.content}}</textarea>
</div>
{{if not .PageIsEditProjects}}
<div class="field">
<label>{{.locale.Tr "repo.projects.card_type.desc"}}</label>
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
<div class="ui selection dropdown">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{range $element := .CardTypes}}
{{if or (eq $.card_type $element.CardType) (and (not $.PageIsEditProjects) (eq $element.CardType 1))}}
<input type="hidden" name="card_type" value="{{$element.CardType}}">
<div class="default text">{{$.locale.Tr $element.Translation}}</div>
{{end}}
{{end}}
<input type="hidden" name="board_type" value="{{.type}}">
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
<div class="menu">
{{range $element := .CardTypes}}
<div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div>
{{range $element := .BoardTypes}}
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui left">
{{if .PageIsEditProjects}}
<a class="ui cancel button" href="{{$.HomeLink}}/-/projects{{if eq .redirect "project"}}/{{.projectID}}{{end}}">
{{.locale.Tr "repo.milestones.cancel"}}
</a>
<button class="ui primary button">
{{.locale.Tr "repo.projects.modify"}}
</button>
{{else}}
<button class="ui primary button">
{{.locale.Tr "repo.projects.create"}}
</button>
{{end}}
<div class="field">
<label>{{.locale.Tr "repo.projects.card_type.desc"}}</label>
<div class="ui selection dropdown">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{range $element := .CardTypes}}
{{if or (eq $.card_type $element.CardType) (and (not $.PageIsEditProjects) (eq $element.CardType 1))}}
<input type="hidden" name="card_type" value="{{$element.CardType}}">
<div class="default text">{{$.locale.Tr $element.Translation}}</div>
{{end}}
{{end}}
<div class="menu">
{{range $element := .CardTypes}}
<div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
</form>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui left">
<a class="ui cancel button" href="{{$.CancelLink}}">
{{.locale.Tr "repo.milestones.cancel"}}
</a>
<button class="ui primary button">
{{if .PageIsEditProjects}}{{.locale.Tr "repo.projects.modify"}}{{else}}{{.locale.Tr "repo.projects.create"}}{{end}}
</button>
</div>
</div>
</form>
</div>

View File

@ -15,26 +15,27 @@
<div>
<div class="diff-detail-box diff-box sticky gt-df gt-sb gt-ac gt-fw">
<div class="gt-df gt-ac gt-fw">
<button class="diff-toggle-file-tree-button gt-df gt-ac" data-show-text="{{.locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{.locale.Tr "repo.diff.hide_file_tree"}}">
<button class="diff-toggle-file-tree-button gt-df gt-ac not-mobile" data-show-text="{{.locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{.locale.Tr "repo.diff.hide_file_tree"}}">
{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}}
{{svg "octicon-sidebar-collapse" 20 "icon gt-hidden"}}
{{svg "octicon-sidebar-expand" 20 "icon gt-hidden"}}
</button>
<script>
const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') === 'true';
// Default to true if unset
const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') !== 'false';
const diffTreeBtn = document.querySelector('.diff-toggle-file-tree-button');
const diffTreeIcon = `.octicon-sidebar-${diffTreeVisible ? 'expand' : 'collapse'}`;
diffTreeBtn.querySelector(diffTreeIcon).classList.remove('gt-hidden');
diffTreeBtn.setAttribute('data-tooltip-content', diffTreeBtn.getAttribute(diffTreeVisible ? 'data-hide-text' : 'data-show-text'));
</script>
<div class="diff-detail-stats gt-df gt-ac gt-ml-3">
<div class="diff-detail-stats gt-df gt-ac gt-fw">
{{svg "octicon-diff" 16 "gt-mr-2"}}{{.locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
</div>
</div>
<div class="diff-detail-actions gt-df gt-ac">
<div class="diff-detail-actions gt-df gt-ac gt-gap-2 gt-fw">
{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}}
<progress id="viewed-files-summary" class="gt-mr-2" value="{{.Diff.NumViewedFiles}}" max="{{.Diff.NumFiles}}"></progress>
<label for="viewed-files-summary" id="viewed-files-summary-label" class="gt-mr-3 gt-f1" data-text-changed-template="{{.locale.Tr "repo.pulls.viewed_files_label"}}">
<progress id="viewed-files-summary" value="{{.Diff.NumViewedFiles}}" max="{{.Diff.NumFiles}}"></progress>
<label for="viewed-files-summary" id="viewed-files-summary-label" class="gt-f1 gt-whitespace-nowrap not-mobile" data-text-changed-template="{{.locale.Tr "repo.pulls.viewed_files_label"}}">
{{.locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
</label>
{{end}}
@ -85,9 +86,10 @@
{{$isCsv := (call $.IsCsvFile $file)}}
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived)}}
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-3" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
<h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb">
<div class="diff-file-name gt-df gt-ac gt-mr-3">
<h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb gt-fw">
<div class="diff-file-name gt-df gt-ac gt-gap-2 gt-fw">
<button class="fold-file ui button button-ghost gt-p-0 gt-mr-3{{if not $isExpandable}} gt-invisible{{end}}">
{{if $file.ShouldBeHidden}}
{{svg "octicon-chevron-right" 18}}
@ -105,11 +107,12 @@
{{end}}
</div>
<span class="file gt-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} &rarr; {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} &rarr; {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{$.locale.Tr "repo.stored_lfs"}}){{end}}</span>
<button class="ui button button-link gt-p-3" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button>
{{if $file.IsGenerated}}
<span class="ui label gt-ml-3">{{$.locale.Tr "repo.diff.generated"}}</span>
<span class="ui label">{{$.locale.Tr "repo.diff.generated"}}</span>
{{end}}
{{if $file.IsVendored}}
<span class="ui label gt-ml-3">{{$.locale.Tr "repo.diff.vendored"}}</span>
<span class="ui label">{{$.locale.Tr "repo.diff.vendored"}}</span>
{{end}}
{{if and $file.Mode $file.OldMode}}
<span class="gt-ml-4 gt-mono">{{$file.OldMode}} &rarr; {{$file.Mode}}</span>
@ -117,7 +120,7 @@
<span class="gt-ml-4 gt-mono">{{$file.Mode}}</span>
{{end}}
</div>
<div class="diff-file-header-actions gt-df gt-ac">
<div class="diff-file-header-actions gt-df gt-ac gt-gap-2 gt-fw">
{{if $showFileViewToggle}}
<div class="ui compact icon buttons">
<button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{$.locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button>
@ -127,8 +130,11 @@
{{if $file.IsProtected}}
<span class="ui basic label">{{$.locale.Tr "repo.diff.protected"}}</span>
{{end}}
{{if and $isReviewFile $file.HasChangedSinceLastReview}}
<span class="changed-since-last-review unselectable not-mobile">{{$.locale.Tr "repo.pulls.has_changed_since_last_review"}}</span>
{{end}}
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
<button class="ui basic tiny button unescape-button">{{$.locale.Tr "repo.unescape_control_characters"}}</button>
<button class="ui basic tiny button unescape-button not-mobile">{{$.locale.Tr "repo.unescape_control_characters"}}</button>
<button class="ui basic tiny button escape-button gt-hidden">{{$.locale.Tr "repo.escape_control_characters"}}</button>
{{end}}
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
@ -138,10 +144,7 @@
<a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{$.locale.Tr "repo.diff.view_file"}}</a>
{{end}}
{{end}}
{{if and $.IsSigned $.PageIsPullFiles (not $.IsArchived)}}
{{if $file.HasChangedSinceLastReview}}
<span class="changed-since-last-review unselectable">{{$.locale.Tr "repo.pulls.has_changed_since_last_review"}}</span>
{{end}}
{{if $isReviewFile}}
<label data-link="{{$.Issue.Link}}/viewed-files" data-headcommit="{{$.PullHeadCommitID}}" class="viewed-file-form unselectable{{if $file.IsViewed}} viewed-file-checked-form{{end}}">
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{$.locale.Tr "repo.pulls.has_viewed_file"}}
</label>

View File

@ -3,7 +3,7 @@
{{template "repo/header" .}}
<div class="ui container">
<div class="gt-df">
<h1>{{.Milestone.Name}}</h1>
<h1 class="gt-mb-3">{{.Milestone.Name}}</h1>
{{if not .Repository.IsArchived}}
<div class="text right gt-f1">
{{if or .CanWriteIssues .CanWritePulls}}
@ -20,26 +20,28 @@
</div>
{{end}}
</div>
<div class="ui one column stackable grid">
<div class="column markup content">
{{.Milestone.RenderedContent|Str2html}}
</div>
{{if .Milestone.RenderedContent}}
<div class="markup content gt-mb-4">
{{.Milestone.RenderedContent|Str2html}}
</div>
<div class="ui one column stackable grid">
<div class="column">
{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix $.locale}}
{{if .IsClosed}}
{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate | Safe}}
{{else}}
{{svg "octicon-calendar"}}
{{if .Milestone.DeadlineString}}
<span {{if .IsOverdue}}class="overdue"{{end}}>{{DateTime "short" .Milestone.DeadlineString}}</span>
{{end}}
<div class="gt-df gt-fc gt-gap-3">
<progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress>
<div class="gt-df gt-gap-4">
<div classs="gt-df gt-ac">
{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix $.locale}}
{{if .IsClosed}}
{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate | Safe}}
{{else}}
{{$.locale.Tr "repo.milestones.no_due_date"}}
{{svg "octicon-calendar"}}
{{if .Milestone.DeadlineString}}
<span {{if .IsOverdue}}class="overdue"{{end}}>{{DateTime "short" .Milestone.DeadlineString}}</span>
{{else}}
{{$.locale.Tr "repo.milestones.no_due_date"}}
{{end}}
{{end}}
{{end}}
&nbsp;
<b>{{.locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</b>
</div>
<div class="gt-mr-3">{{.locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
</div>
</div>
<div class="ui divider"></div>

View File

@ -1,92 +1,6 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository projects edit-project new milestone">
{{template "repo/header" .}}
<div class="ui container">
<div class="navbar">
{{template "repo/issue/navbar" .}}
{{if and .CanWriteProjects .PageIsEditProject}}
<div class="ui right floated secondary menu">
<a class="ui small green button" href="{{$.RepoLink}}/projects/new">{{.locale.Tr "repo.milestones.new"}}</a>
</div>
{{end}}
</div>
<div class="ui divider"></div>
<h2 class="ui dividing header">
{{if .PageIsEditProjects}}
{{.locale.Tr "repo.projects.edit"}}
<div class="sub header">{{.locale.Tr "repo.projects.edit_subheader"}}</div>
{{else}}
{{.locale.Tr "repo.projects.new"}}
<div class="sub header">{{.locale.Tr "repo.projects.new_subheader"}}</div>
{{end}}
</h2>
{{template "base/alert" .}}
<form class="ui form grid" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="eleven wide column">
<input type="hidden" id="redirect" name="redirect" value="{{.redirect}}">
<div class="field {{if .Err_Title}}error{{end}}">
<label>{{.locale.Tr "repo.projects.title"}}</label>
<input name="title" placeholder="{{.locale.Tr "repo.projects.title"}}" value="{{.title}}" autofocus required>
</div>
<div class="field">
<label>{{.locale.Tr "repo.projects.description"}}</label>
<textarea name="content" placeholder="{{.locale.Tr "repo.projects.description_placeholder"}}">{{.content}}</textarea>
</div>
{{if not .PageIsEditProjects}}
<div class="field">
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="board_type" value="{{.type}}">
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
<div class="menu">
{{range $element := .BoardTypes}}
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
{{end}}
<div class="field">
<label>{{.locale.Tr "repo.projects.card_type.desc"}}</label>
<div class="ui selection dropdown">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{range $element := .CardTypes}}
{{if or (eq $.card_type $element.CardType) (and (not $.PageIsEditProjects) (eq $element.CardType 1))}}
<input type="hidden" name="card_type" value="{{$element.CardType}}">
<div class="default text">{{$.locale.Tr $element.Translation}}</div>
{{end}}
{{end}}
<div class="menu">
{{range $element := .CardTypes}}
<div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui left">
{{if .PageIsEditProjects}}
<a class="ui cancel button" href="{{.RepoLink}}/projects{{if eq .redirect "project"}}/{{.projectID}}{{end}}">
{{.locale.Tr "repo.milestones.cancel"}}
</a>
<button class="ui primary button">
{{.locale.Tr "repo.projects.modify"}}
</button>
{{else}}
<button class="ui primary button">
{{.locale.Tr "repo.projects.create"}}
</button>
{{end}}
</div>
</div>
</form>
</div>
{{template "projects/new" .}}
</div>
{{template "base/footer" .}}

View File

@ -256,7 +256,6 @@
<div class="field">
<button class="ui green button">{{$.locale.Tr "repo.settings.protected_branch.save_rule"}}</button>
<button class="ui gray button">{{$.locale.Tr "cancel"}}</button>
</div>
</div>
</form>

View File

@ -5,40 +5,40 @@
<div class="ui stackable grid">
<div class="four wide column">
<div class="ui secondary vertical filter menu gt-bg-transparent">
<a class="{{if eq .ViewType "your_repositories"}}ui basic primary button{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "home.issues.in_your_repos"}}
<strong class="ui right">{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
</a>
<a class="{{if eq .ViewType "assigned"}}ui basic primary button{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "repo.issues.filter_type.assigned_to_you"}}
<strong class="ui right">{{CountFmt .IssueStats.AssignCount}}</strong>
</a>
<a class="{{if eq .ViewType "created_by"}}ui basic primary button{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "repo.issues.filter_type.created_by_you"}}
<strong class="ui right">{{CountFmt .IssueStats.CreateCount}}</strong>
</a>
{{if .PageIsPulls}}
<a class="{{if eq .ViewType "review_requested"}}ui basic primary button{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "repo.issues.filter_type.review_requested"}}
<strong class="ui right">{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
</a>
<a class="{{if eq .ViewType "reviewed_by"}}ui basic primary button{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
<strong class="ui right">{{CountFmt .IssueStats.ReviewedCount}}</strong>
</a>
{{end}}
<a class="{{if eq .ViewType "mentioned"}}ui basic primary button{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
{{.locale.Tr "repo.issues.filter_type.mentioning_you"}}
<strong class="ui right">{{CountFmt .IssueStats.MentionCount}}</strong>
</a>
<div class="ui divider"></div>
<a class="{{if not $.RepoIDs}}ui basic primary button{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}">
<a class="{{if not $.RepoIDs}}active{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&q={{$.Keyword}}">
<span class="text truncate">All</span>
<span>{{CountFmt .TotalIssueCount}}</span>
</a>
{{range .Repos}}
{{with $Repo := .}}
<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}ui basic primary button{{end}}{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&repos=[
<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}&repos=[
{{- with $include := true -}}
{{- range $.RepoIDs -}}
{{- if eq . $Repo.ID -}}

View File

@ -12,7 +12,7 @@
<div class="ui divider"></div>
{{range .Repos}}
{{with $Repo := .}}
<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}ui basic primary button{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
{{- with $include := true -}}
{{- range $.RepoIDs -}}
{{- if eq . $Repo.ID -}}

View File

@ -1,69 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository new repo">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{.locale.Tr "new_project"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{.locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
{{avatar $.Context .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
{{avatar $.Context .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div>
{{range .Orgs}}
<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
{{avatar $.Context . 28 "mini"}}
<span class="truncated-item-name">{{.ShortName 40}}</span>
</div>
{{end}}
</div>
</div>
</div>
<div class="inline field {{if .Err_Title}}error{{end}}">
<label>{{.locale.Tr "repo.projects.title"}}</label>
<input name="title" placeholder="{{.locale.Tr "repo.projects.title"}}" value="{{.title}}" autofocus required>
</div>
<div class="inline field">
<label>{{.locale.Tr "repo.projects.desc"}}</label>
<textarea name="content">{{.content}}</textarea>
</div>
<div class="inline field">
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="board_type" value="{{.type}}">
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
<div class="menu">
{{range $element := .BoardTypes}}
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">
{{.locale.Tr "repo.projects.create"}}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -18,13 +18,21 @@
</div>
<i class="text {{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-tooltip-content="{{$.locale.Tr "settings.token_state_desc"}}"{{end}}>{{svg "fontawesome-send" 36}}</i>
<div class="content">
<!--Temporarily disable-->
<strong>{{.Name}}</strong>
<details class="gt-hidden"><summary><strong>{{.Name}}</strong></summary>
<p class="gt-my-2">{{$.locale.Tr "settings.scopes_list"}}</p>
<details><summary><strong>{{.Name}}</strong></summary>
<p class="gt-my-2">
{{$.locale.Tr "settings.repo_and_org_access"}}:
{{if .DisplayPublicOnly}}
{{$.locale.Tr "settings.permissions_public_only"}}
{{else}}
{{$.locale.Tr "settings.permissions_access_all"}}
{{end}}
</p>
<p class="gt-my-2">{{$.locale.Tr "settings.permissions_list"}}</p>
<ul class="gt-my-2">
{{range .Scope.StringSlice}}
<li>{{.}}</li>
{{if (ne . $.AccessTokenScopePublicOnly)}}
<li>{{.}}</li>
{{end}}
{{end}}
</ul>
</details>
@ -40,222 +48,46 @@
<h5 class="ui top header">
{{.locale.Tr "settings.generate_new_token"}}
</h5>
<p>{{.locale.Tr "settings.new_token_desc"}}</p>
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_Name}}error{{end}}">
<label for="name">{{.locale.Tr "settings.token_name"}}</label>
<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
</div>
<!--Temporarily disable-->
<details class="gt-hidden ui optional field">
<summary class="gt-p-2">
{{.locale.Tr "settings.select_scopes"}}
<div class="field">
<label>{{.locale.Tr "settings.repo_and_org_access"}}</label>
<label class="gt-cursor-pointer">
<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
{{.locale.Tr "settings.permissions_public_only"}}
</label>
<label class="gt-cursor-pointer">
<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked>
{{.locale.Tr "settings.permissions_access_all"}}
</label>
</div>
<details class="ui optional field">
<summary class="gt-pb-4 gt-pl-2">
{{.locale.Tr "settings.select_permissions"}}
</summary>
<div class="field gt-pl-2">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="repo">
<label>repo</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="repo:status">
<label>repo:status</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="public_repo">
<label>public_repo</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:org">
<label>admin:org</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:org">
<label>write:org</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:org">
<label>read:org</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:public_key">
<label>admin:public_key</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:public_key">
<label>write:public_key</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:public_key">
<label>read:public_key</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:repo_hook">
<label>admin:repo_hook</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:repo_hook">
<label>write:repo_hook</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:repo_hook">
<label>read:repo_hook</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:org_hook">
<label>admin:org_hook</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:user_hook">
<label>admin:user_hook</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="notification">
<label>notification</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="user">
<label>user</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:user">
<label>read:user</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="user:email">
<label>user:email</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="user:follow">
<label>user:follow</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="delete_repo">
<label>delete_repo</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="package">
<label>package</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:package">
<label>write:package</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:package">
<label>read:package</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="delete:package">
<label>delete:package</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:gpg_key">
<label>admin:gpg_key</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:gpg_key">
<label>write:gpg_key</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:gpg_key">
<label>read:gpg_key</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="admin:application">
<label>admin:application</label>
</div>
</div>
<div class="field gt-pl-4">
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="write:application">
<label>write:application</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="read:application">
<label>read:application</label>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input class="enable-system" type="checkbox" name="scope" value="sudo">
<label>sudo</label>
</div>
<div class="activity meta">
<i>{{$.locale.Tr "settings.scoped_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
</div>
<scoped-access-token-category category="activitypub"></scoped-access-token-category>
{{if .IsAdmin}}
<scoped-access-token-category category="admin"></scoped-access-token-category>
{{end}}
<scoped-access-token-category category="issue"></scoped-access-token-category>
<scoped-access-token-category category="misc"></scoped-access-token-category>
<scoped-access-token-category category="notification"></scoped-access-token-category>
<scoped-access-token-category category="organization"></scoped-access-token-category>
<scoped-access-token-category category="package"></scoped-access-token-category>
<scoped-access-token-category category="repository"></scoped-access-token-category>
<scoped-access-token-category category="user"></scoped-access-token-category>
</details>
<button class="ui green button">
<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden">
{{.locale.Tr "settings.at_least_one_permission"}}
</div>
<button id="scoped-access-submit" class="ui green button">
{{.locale.Tr "settings.generate_token"}}
</button>
</form>

View File

@ -39,8 +39,8 @@
<input id="application-name" value="{{.App.Name}}" name="application_name" required maxlength="255">
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label>
<input type="url" name="redirect_uri" value="{{.App.PrimaryRedirectURI}}" id="redirect-uri" required>
<label for="redirect-uris">{{.locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris" required>{{StringUtils.Join .App.RedirectURIs "\n"}}</textarea>
</div>
<div class="field ui checkbox {{if .Err_ConfidentialClient}}error{{end}}">
<label>{{.locale.Tr "settings.oauth2_confidential_client"}}</label>

View File

@ -34,8 +34,8 @@
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uri">{{.locale.Tr "settings.oauth2_redirect_uri"}}</label>
<input type="url" name="redirect_uri" id="redirect-uri">
<label for="redirect-uris">{{.locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris"></textarea>
</div>
<div class="field ui checkbox {{if .Err_ConfidentialClient}}error{{end}}">
<label>{{.locale.Tr "settings.oauth2_confidential_client"}}</label>

View File

@ -25,16 +25,16 @@
</div>
{{if .AccountLinks}}
{{range $loginSource, $provider := .AccountLinks}}
<div class="item">
<div class="right floated content">
<button class="ui red tiny button delete-button" data-modal-id="delete-account-link" data-url="{{AppSubUrl}}/user/settings/security/account_link" data-id="{{$loginSource.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
<div class="content">
<strong>{{$provider}}</strong>
{{if $loginSource.IsActive}}<span class="text red">{{$.locale.Tr "repo.settings.active"}}</span>{{end}}
<div class="item gt-df gt-ac">
<div class="gt-f1">
<span data-tooltip-content="{{$provider}}">
{{$loginSource.Name}}
{{if $loginSource.IsActive}}<span class="text primary">{{$.locale.Tr "repo.settings.active"}}</span>{{end}}
</span>
</div>
<button class="ui red tiny button delete-button" data-modal-id="delete-account-link" data-url="{{AppSubUrl}}/user/settings/security/account_link" data-id="{{$loginSource.ID}}">
{{$.locale.Tr "settings.delete_key"}}
</button>
</div>
{{end}}
{{end}}

View File

@ -21,7 +21,7 @@ import (
func TestAPIAdminOrgCreate(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
org := api.CreateOrgOption{
UserName: "user2_org",
@ -55,7 +55,7 @@ func TestAPIAdminOrgCreate(t *testing.T) {
func TestAPIAdminOrgCreateBadVisibility(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
org := api.CreateOrgOption{
UserName: "user2_org",

View File

@ -25,7 +25,7 @@ func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
session := loginUser(t, "user1")
keyOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", keyOwner.Name, token)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
@ -52,7 +52,7 @@ func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// user1 is an admin user
token := getUserToken(t, "user1", auth_model.AccessTokenScopeSudo)
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteAdmin)
req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d?token=%s", unittest.NonexistentID, token)
MakeRequest(t, req, http.StatusNotFound)
}
@ -61,7 +61,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
normalUsername := "user2"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", adminUsername, token)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
@ -82,7 +82,7 @@ func TestAPISudoUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
normalUsername := "user2"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", normalUsername, token)
req := NewRequest(t, "GET", urlStr)
@ -98,7 +98,7 @@ func TestAPISudoUserForbidden(t *testing.T) {
adminUsername := "user1"
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadAdmin)
urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", adminUsername, token)
req := NewRequest(t, "GET", urlStr)
MakeRequest(t, req, http.StatusForbidden)
@ -107,7 +107,7 @@ func TestAPISudoUserForbidden(t *testing.T) {
func TestAPIListUsers(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
req := NewRequest(t, "GET", urlStr)
@ -143,7 +143,7 @@ func TestAPIListUsersNonAdmin(t *testing.T) {
func TestAPICreateUserInvalidEmail(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"email": "invalid_email@domain.com\r\n",
@ -161,7 +161,7 @@ func TestAPICreateUserInvalidEmail(t *testing.T) {
func TestAPICreateAndDeleteUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
req := NewRequestWithValues(
t,
@ -187,7 +187,7 @@ func TestAPICreateAndDeleteUser(t *testing.T) {
func TestAPIEditUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s?token=%s", "user2", token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@ -229,7 +229,7 @@ func TestAPIEditUser(t *testing.T) {
func TestAPICreateRepoForUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
req := NewRequestWithJSON(
t,
@ -245,7 +245,7 @@ func TestAPICreateRepoForUser(t *testing.T) {
func TestAPIRenameUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "user2", token)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
// required

View File

@ -16,7 +16,7 @@ import (
)
func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
resp := MakeRequest(t, req, NoExpectedStatus)
if !exists {
@ -32,7 +32,7 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
}
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
resp := MakeRequest(t, req, expectedHTTPStatus)
@ -44,7 +44,7 @@ func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPSta
}
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
RuleName: branchName,
})
@ -58,7 +58,7 @@ func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTP
}
func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
resp := MakeRequest(t, req, expectedHTTPStatus)
@ -70,13 +70,13 @@ func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.Bran
}
func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
MakeRequest(t, req, expectedHTTPStatus)
}
func testAPIDeleteBranch(t *testing.T, branchName string, expectedHTTPStatus int) {
token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
MakeRequest(t, req, expectedHTTPStatus)
}
@ -102,7 +102,7 @@ func TestAPICreateBranch(t *testing.T) {
func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
username := "user2"
ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeRepo)
ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
giteaURL.Path = ctx.GitPath()
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
@ -149,7 +149,7 @@ func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
}
func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch string, status int) bool {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+repo+"/branches?token="+token, &api.CreateBranchRepoOption{
BranchName: newBranch,
OldBranchName: oldBranch,

View File

@ -36,8 +36,8 @@ func TestAPIGetCommentAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
session.MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
resp := session.MakeRequest(t, req, http.StatusOK)
@ -61,8 +61,9 @@ func TestAPIListCommentAttachments(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets",
repoOwner.Name, repo.Name, comment.ID)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
resp := session.MakeRequest(t, req, http.StatusOK)
var apiAttachments []*api.Attachment
@ -82,7 +83,7 @@ func TestAPICreateCommentAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
@ -121,7 +122,7 @@ func TestAPIEditCommentAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@ -144,7 +145,7 @@ func TestAPIDeleteCommentAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)

View File

@ -76,7 +76,7 @@ func TestAPIListIssueComments(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
resp := MakeRequest(t, req, http.StatusOK)
@ -96,7 +96,7 @@ func TestAPICreateComment(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
@ -118,7 +118,7 @@ func TestAPIGetComment(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, token)
@ -146,7 +146,7 @@ func TestAPIEditComment(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@ -170,7 +170,7 @@ func TestAPIDeleteComment(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
MakeRequest(t, req, http.StatusNoContent)

View File

@ -21,8 +21,8 @@ type makeRequestFunc func(testing.TB, *http.Request, int) *httptest.ResponseReco
func TestGPGKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
tokenWithGPGKeyScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminGPGKey, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
tokenWithGPGKeyScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
tt := []struct {
name string
@ -36,7 +36,7 @@ func TestGPGKeys(t *testing.T) {
},
{
name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token,
results: []int{http.StatusForbidden, http.StatusOK, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
results: []int{http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
},
{
name: "LoggedAsUser2WithScope", makeRequest: session.MakeRequest, token: tokenWithGPGKeyScope,

View File

@ -53,7 +53,7 @@ func TestHTTPSigPubKey(t *testing.T) {
// Add our public key to user1
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminPublicKey, auth_model.AccessTokenScopeSudo))
token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser))
keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
keyType := "ssh-rsa"
keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqd"
@ -69,7 +69,8 @@ func TestHTTPSigPubKey(t *testing.T) {
keyID := ssh.FingerprintSHA256(sshSigner.PublicKey())
// create the request
req = NewRequest(t, "GET", "/api/v1/admin/users")
token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/users?token=%s", token))
signer, _, err := httpsig.NewSSHSigner(sshSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)"}, httpsig.Signature, 10)
if err != nil {

View File

@ -32,7 +32,7 @@ func TestAPIGetIssueAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
@ -53,7 +53,7 @@ func TestAPIListIssueAttachments(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
@ -73,7 +73,7 @@ func TestAPICreateIssueAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
@ -111,7 +111,7 @@ func TestAPIEditIssueAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@ -133,7 +133,7 @@ func TestAPIDeleteIssueAttachment(t *testing.T) {
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)

View File

@ -25,7 +25,7 @@ func TestAPIModifyLabels(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels?token=%s", owner.Name, repo.Name, token)
// CreateLabel
@ -97,7 +97,7 @@ func TestAPIAddIssueLabels(t *testing.T) {
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
repo.OwnerName, repo.Name, issue.Index, token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
@ -120,7 +120,7 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
owner.Name, repo.Name, issue.Index, token)
req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{
@ -144,7 +144,7 @@ func TestAPIModifyOrgLabels(t *testing.T) {
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
user := "user1"
session := loginUser(t, user)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeAdminOrg)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)
// CreateLabel

Some files were not shown because too many files have changed in this diff Show More