diff --git a/cmd/admin.go b/cmd/admin.go
index a662011f9c0..8f8c68f9810 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -180,6 +180,11 @@ var (
Name: "raw",
Usage: "Display only the token value",
},
+ cli.StringFlag{
+ Name: "scopes",
+ Value: "",
+ Usage: "Comma separated list of scopes to apply to access token",
+ },
},
Action: runGenerateAccessToken,
}
@@ -698,9 +703,15 @@ func runGenerateAccessToken(c *cli.Context) error {
return err
}
+ accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize()
+ if err != nil {
+ return err
+ }
+
t := &auth_model.AccessToken{
- Name: c.String("token-name"),
- UID: user.ID,
+ Name: c.String("token-name"),
+ UID: user.ID,
+ Scope: accessTokenScope,
}
if err := auth_model.NewAccessToken(t); err != nil {
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index daf67ef4c6b..77efe1417ba 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2458,6 +2458,8 @@ ROUTER = console
;LIMIT_SIZE_COMPOSER = -1
;; Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_CONAN = -1
+;; Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_CONDA = -1
;; Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_CONTAINER = -1
;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 4ef630b6be0..441bb824ad2 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -1214,6 +1214,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_CONAN`: **-1**: Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_CONDA`: **-1**: Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
- `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
diff --git a/docs/content/doc/features/authentication.en-us.md b/docs/content/doc/features/authentication.en-us.md
index f25065d9c48..c27a09b00bf 100644
--- a/docs/content/doc/features/authentication.en-us.md
+++ b/docs/content/doc/features/authentication.en-us.md
@@ -329,3 +329,22 @@ Before activating SSPI single sign-on authentication (SSO) you have to prepare y
- You have added the URL of the web app to the `Local intranet zone`
- The clocks of the server and client should not differ with more than 5 minutes (depends on group policy)
- `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`)
+
+## Reverse Proxy
+
+Gitea supports Reverse Proxy Header authentication, it will read headers as a trusted login user name or user email address. This hasn't been enabled by default, you can enable it with
+
+```ini
+[service]
+ENABLE_REVERSE_PROXY_AUTHENTICATION = true
+```
+
+The default login user name is in the `X-WEBAUTH-USER` header, you can change it via changing `REVERSE_PROXY_AUTHENTICATION_USER` in app.ini. If the user doesn't exist, you can enable automatic registration with `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true`.
+
+The default login user email is `X-WEBAUTH-EMAIL`, you can change it via changing `REVERSE_PROXY_AUTHENTICATION_EMAIL` in app.ini, this could also be disabled with `ENABLE_REVERSE_PROXY_EMAIL`
+
+If set `ENABLE_REVERSE_PROXY_FULL_NAME=true`, a user full name expected in `X-WEBAUTH-FULLNAME` will be assigned to the user when auto creating the user. You can also change the header name with `REVERSE_PROXY_AUTHENTICATION_FULL_NAME`.
+
+You can also limit the reverse proxy's IP address range with `REVERSE_PROXY_TRUSTED_PROXIES` which default value is `127.0.0.0/8,::1/128`. By `REVERSE_PROXY_LIMIT`, you can limit trusted proxies level.
+
+Notice: Reverse Proxy Auth doesn't support the API. You still need an access token or basic auth to make API requests.
diff --git a/docs/content/doc/features/authentication.zh-cn.md b/docs/content/doc/features/authentication.zh-cn.md
index 481e33441b5..aeb099f760b 100644
--- a/docs/content/doc/features/authentication.zh-cn.md
+++ b/docs/content/doc/features/authentication.zh-cn.md
@@ -15,4 +15,21 @@ menu:
# 认证
-## TBD
+## 反向代理认证
+
+Gitea 支持通过读取反向代理传递的 HTTP 头中的登录名或者 email 地址来支持反向代理来认证。默认是不启用的,你可以用以下配置启用。
+
+```ini
+[service]
+ENABLE_REVERSE_PROXY_AUTHENTICATION = true
+```
+
+默认的登录用户名的 HTTP 头是 `X-WEBAUTH-USER`,你可以通过修改 `REVERSE_PROXY_AUTHENTICATION_USER` 来变更它。如果用户不存在,可以自动创建用户,当然你需要修改 `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true` 来启用它。
+
+默认的登录用户 Email 的 HTTP 头是 `X-WEBAUTH-EMAIL`,你可以通过修改 `REVERSE_PROXY_AUTHENTICATION_EMAIL` 来变更它。如果用户不存在,可以自动创建用户,当然你需要修改 `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true` 来启用它。你也可以通过修改 `ENABLE_REVERSE_PROXY_EMAIL` 来启用或停用这个 HTTP 头。
+
+如果设置了 `ENABLE_REVERSE_PROXY_FULL_NAME=true`,则用户的全名会从 `X-WEBAUTH-FULLNAME` 读取,这样在自动创建用户时将使用这个字段作为用户全名,你也可以通过修改 `REVERSE_PROXY_AUTHENTICATION_FULL_NAME` 来变更 HTTP 头。
+
+你也可以通过修改 `REVERSE_PROXY_TRUSTED_PROXIES` 来设置反向代理的IP地址范围,加强安全性,默认值是 `127.0.0.0/8,::1/128`。 通过 `REVERSE_PROXY_LIMIT`, 可以设置最多信任几级反向代理。
+
+注意:反向代理认证不支持认证 API,API 仍旧需要用 access token 来进行认证。
diff --git a/docs/content/doc/packages/conda.en-us.md b/docs/content/doc/packages/conda.en-us.md
new file mode 100644
index 00000000000..8b828475904
--- /dev/null
+++ b/docs/content/doc/packages/conda.en-us.md
@@ -0,0 +1,85 @@
+---
+date: "2022-12-28T00:00:00+00:00"
+title: "Conda Packages Repository"
+slug: "packages/conda"
+draft: false
+toc: false
+menu:
+ sidebar:
+ parent: "packages"
+ name: "Conda"
+ weight: 25
+ identifier: "conda"
+---
+
+# Conda Packages Repository
+
+Publish [Conda](https://docs.conda.io/en/latest/) packages for your user or organization.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Requirements
+
+To work with the Conda package registry, you need to use [conda](https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html).
+
+## Configuring the package registry
+
+To register the package registry and provide credentials, edit your `.condarc` file:
+
+```yaml
+channel_alias: https://gitea.example.com/api/packages/{owner}/conda
+channels:
+ - https://gitea.example.com/api/packages/{owner}/conda
+default_channels:
+ - https://gitea.example.com/api/packages/{owner}/conda
+```
+
+| Placeholder | Description |
+| ------------ | ----------- |
+| `owner` | The owner of the package. |
+
+See the [official documentation](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html) for explanations of the individual settings.
+
+If you need to provide credentials, you may embed them as part of the channel url (`https://user:password@gitea.example.com/...`).
+
+## Publish a package
+
+To publish a package, perform a HTTP PUT operation with the package content in the request body.
+
+```
+PUT https://gitea.example.com/api/packages/{owner}/conda/{channel}/{filename}
+```
+
+| Placeholder | Description |
+| ------------ | ----------- |
+| `owner` | The owner of the package. |
+| `channel` | The [channel](https://conda.io/projects/conda/en/latest/user-guide/concepts/channels.html) of the package. (optional) |
+| `filename` | The name of the file. |
+
+Example request using HTTP Basic authentication:
+
+```shell
+curl --user your_username:your_password_or_token \
+ --upload-file path/to/package-1.0.conda \
+ https://gitea.example.com/api/packages/testuser/conda/package-1.0.conda
+```
+
+You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
+
+## Install a package
+
+To install a package from the package registry, execute one of the following commands:
+
+```shell
+conda install {package_name}
+conda install {package_name}={package_version}
+conda install -c {channel} {package_name}
+```
+
+| Parameter | Description |
+| ----------------- | ----------- |
+| `package_name` | The package name. |
+| `package_version` | The package version. |
+| `channel` | The channel of the package. (optional) |
diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md
index b12155e14da..9a736c1e564 100644
--- a/docs/content/doc/packages/overview.en-us.md
+++ b/docs/content/doc/packages/overview.en-us.md
@@ -28,6 +28,7 @@ The following package managers are currently supported:
| ---- | -------- | -------------- |
| [Composer]({{< relref "doc/packages/composer.en-us.md" >}}) | PHP | `composer` |
| [Conan]({{< relref "doc/packages/conan.en-us.md" >}}) | C++ | `conan` |
+| [Conda]({{< relref "doc/packages/conda.en-us.md" >}}) | - | `conda` |
| [Container]({{< relref "doc/packages/container.en-us.md" >}}) | - | any OCI compliant client |
| [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) | - | any HTTP client |
| [Helm]({{< relref "doc/packages/helm.en-us.md" >}}) | - | any HTTP client, `cm-push` |
diff --git a/go.mod b/go.mod
index b3d40403235..a929508e0de 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1
+ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
github.com/dustin/go-humanize v1.0.0
github.com/editorconfig/editorconfig-core-go/v2 v2.5.1
github.com/emersion/go-imap v1.2.1
@@ -161,7 +162,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
- github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
diff --git a/models/asymkey/error.go b/models/asymkey/error.go
index 1d486082f46..03bc82302f1 100644
--- a/models/asymkey/error.go
+++ b/models/asymkey/error.go
@@ -24,6 +24,9 @@ func (err ErrKeyUnableVerify) Error() string {
return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
}
+// ErrKeyIsPrivate is returned when the provided key is a private key not a public key
+var ErrKeyIsPrivate = util.NewSilentWrapErrorf(util.ErrInvalidArgument, "the provided key is a private key")
+
// ErrKeyNotExist represents a "KeyNotExist" kind of error.
type ErrKeyNotExist struct {
ID int64
diff --git a/models/asymkey/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go
index 1df6db6fa72..8693c87e76b 100644
--- a/models/asymkey/ssh_key_parse.go
+++ b/models/asymkey/ssh_key_parse.go
@@ -96,6 +96,9 @@ func parseKeyString(content string) (string, error) {
if block == nil {
return "", fmt.Errorf("failed to parse PEM block containing the public key")
}
+ if strings.Contains(block.Type, "PRIVATE") {
+ return "", ErrKeyIsPrivate
+ }
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
diff --git a/models/dbfs/dbfs.go b/models/dbfs/dbfs.go
index 89d3dc7cbc9..6b5b3beeb27 100644
--- a/models/dbfs/dbfs.go
+++ b/models/dbfs/dbfs.go
@@ -10,6 +10,35 @@ import (
"code.gitea.io/gitea/models/db"
)
+/*
+The reasons behind the DBFS (database-filesystem) package:
+When a Gitea action is running, the Gitea action server should collect and store all the logs.
+
+The requirements are:
+* The running logs must be stored across the cluster if the Gitea servers are deployed as a cluster.
+* The logs will be archived to Object Storage (S3/MinIO, etc.) after a period of time.
+* The Gitea action UI should be able to render the running logs and the archived logs.
+
+Some possible solutions for the running logs:
+* [Not ideal] Using local temp file: it can not be shared across the cluster.
+* [Not ideal] Using shared file in the filesystem of git repository: although at the moment, the Gitea cluster's
+ git repositories must be stored in a shared filesystem, in the future, Gitea may need a dedicated Git Service Server
+ to decouple the shared filesystem. Then the action logs will become a blocker.
+* [Not ideal] Record the logs in a database table line by line: it has a couple of problems:
+ - It's difficult to make multiple increasing sequence (log line number) for different databases.
+ - The database table will have a lot of rows and be affected by the big-table performance problem.
+ - It's difficult to load logs by using the same interface as other storages.
+ - It's difficult to calculate the size of the logs.
+
+The DBFS solution:
+* It can be used in a cluster.
+* It can share the same interface (Read/Write/Seek) as other storages.
+* It's very friendly to database because it only needs to store much fewer rows than the log-line solution.
+* In the future, when Gitea action needs to limit the log size (other CI/CD services also do so), it's easier to calculate the log file size.
+* Even sometimes the UI needs to render the tailing lines, the tailing lines can be found be counting the "\n" from the end of the file by seek.
+ The seeking and finding is not the fastest way, but it's still acceptable and won't affect the performance too much.
+*/
+
type dbfsMeta struct {
ID int64 `xorm:"pk autoincr"`
FullPath string `xorm:"VARCHAR(500) UNIQUE NOT NULL"`
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index 5d4781ad44f..ef3cfbbbec7 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -4,6 +4,7 @@
owner_name: user2
lower_name: repo1
name: repo1
+ default_branch: master
num_watches: 4
num_stars: 0
num_forks: 0
@@ -34,6 +35,7 @@
owner_name: user2
lower_name: repo2
name: repo2
+ default_branch: master
num_watches: 0
num_stars: 1
num_forks: 0
@@ -64,6 +66,7 @@
owner_name: user3
lower_name: repo3
name: repo3
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -94,6 +97,7 @@
owner_name: user5
lower_name: repo4
name: repo4
+ default_branch: master
num_watches: 0
num_stars: 1
num_forks: 0
@@ -274,6 +278,7 @@
owner_name: user12
lower_name: repo10
name: repo10
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 1
@@ -304,6 +309,7 @@
owner_name: user13
lower_name: repo11
name: repo11
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -425,6 +431,7 @@
owner_name: user2
lower_name: repo15
name: repo15
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -455,6 +462,7 @@
owner_name: user2
lower_name: repo16
name: repo16
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -905,6 +913,7 @@
owner_name: user2
lower_name: repo20
name: repo20
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -965,6 +974,7 @@
owner_name: user2
lower_name: utf8
name: utf8
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1055,6 +1065,7 @@
owner_name: user2
lower_name: commits_search_test
name: commits_search_test
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1085,6 +1096,7 @@
owner_name: user2
lower_name: git_hooks_test
name: git_hooks_test
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1115,6 +1127,7 @@
owner_name: limited_org
lower_name: public_repo_on_limited_org
name: public_repo_on_limited_org
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1145,6 +1158,7 @@
owner_name: limited_org
lower_name: private_repo_on_limited_org
name: private_repo_on_limited_org
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1175,6 +1189,7 @@
owner_name: privated_org
lower_name: public_repo_on_private_org
name: public_repo_on_private_org
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1205,6 +1220,7 @@
owner_name: privated_org
lower_name: private_repo_on_private_org
name: private_repo_on_private_org
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1235,6 +1251,7 @@
owner_name: user2
lower_name: glob
name: glob
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1295,6 +1312,7 @@
owner_name: user27
lower_name: template1
name: template1
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1355,6 +1373,7 @@
owner_name: org26
lower_name: repo_external_tracker
name: repo_external_tracker
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1385,6 +1404,7 @@
owner_name: org26
lower_name: repo_external_tracker_numeric
name: repo_external_tracker_numeric
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1415,6 +1435,7 @@
owner_name: org26
lower_name: repo_external_tracker_alpha
name: repo_external_tracker_alpha
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1445,6 +1466,7 @@
owner_name: user27
lower_name: repo49
name: repo49
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1475,6 +1497,7 @@
owner_name: user30
lower_name: repo50
name: repo50
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1505,6 +1528,7 @@
owner_name: user30
lower_name: repo51
name: repo51
+ default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
@@ -1565,6 +1589,7 @@
owner_name: user30
lower_name: renderer
name: renderer
+ default_branch: master
is_archived: false
is_empty: false
is_private: false
@@ -1592,6 +1617,7 @@
owner_name: user2
lower_name: lfs
name: lfs
+ default_branch: master
is_empty: false
is_archived: false
is_private: true
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index 12dbff107d1..007c2fd9033 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -173,8 +173,9 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error {
for i := range issues {
set[issues[i].ID] = issues[i]
}
- for i := range prs {
- prs[i].Issue = set[prs[i].IssueID]
+ for _, pr := range prs {
+ pr.Issue = set[pr.IssueID]
+ pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
}
return nil
}
diff --git a/models/packages/conda/search.go b/models/packages/conda/search.go
new file mode 100644
index 00000000000..887441e3b2b
--- /dev/null
+++ b/models/packages/conda/search.go
@@ -0,0 +1,63 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conda
+
+import (
+ "context"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ conda_module "code.gitea.io/gitea/modules/packages/conda"
+
+ "xorm.io/builder"
+)
+
+type FileSearchOptions struct {
+ OwnerID int64
+ Channel string
+ Subdir string
+ Filename string
+}
+
+// SearchFiles gets all files matching the search options
+func SearchFiles(ctx context.Context, opts *FileSearchOptions) ([]*packages.PackageFile, error) {
+ var cond builder.Cond = builder.Eq{
+ "package.type": packages.TypeConda,
+ "package.owner_id": opts.OwnerID,
+ "package_version.is_internal": false,
+ }
+
+ if opts.Filename != "" {
+ cond = cond.And(builder.Eq{
+ "package_file.lower_name": strings.ToLower(opts.Filename),
+ })
+ }
+
+ var versionPropsCond builder.Cond = builder.Eq{
+ "package_property.ref_type": packages.PropertyTypePackage,
+ "package_property.name": conda_module.PropertyChannel,
+ "package_property.value": opts.Channel,
+ }
+
+ cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(versionPropsCond).From("package_property")))
+
+ var filePropsCond builder.Cond = builder.Eq{
+ "package_property.ref_type": packages.PropertyTypeFile,
+ "package_property.name": conda_module.PropertySubdir,
+ "package_property.value": opts.Subdir,
+ }
+
+ cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(filePropsCond).From("package_property")))
+
+ sess := db.GetEngine(ctx).
+ Select("package_file.*").
+ Table("package_file").
+ Join("INNER", "package_version", "package_version.id = package_file.version_id").
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Where(cond)
+
+ pfs := make([]*packages.PackageFile, 0, 10)
+ return pfs, sess.Find(&pfs)
+}
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index 34f1cad87dc..3b36ee22661 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/modules/packages/conda"
"code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/packages/helm"
"code.gitea.io/gitea/modules/packages/maven"
@@ -132,6 +133,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
metadata = &composer.Metadata{}
case TypeConan:
metadata = &conan.Metadata{}
+ case TypeConda:
+ metadata = &conda.VersionMetadata{}
case TypeContainer:
metadata = &container.Metadata{}
case TypeGeneric:
diff --git a/models/packages/package.go b/models/packages/package.go
index a804f35de35..0015953d81d 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -32,6 +32,7 @@ type Type string
const (
TypeComposer Type = "composer"
TypeConan Type = "conan"
+ TypeConda Type = "conda"
TypeContainer Type = "container"
TypeGeneric Type = "generic"
TypeHelm Type = "helm"
@@ -47,6 +48,7 @@ const (
var TypeList = []Type{
TypeComposer,
TypeConan,
+ TypeConda,
TypeContainer,
TypeGeneric,
TypeHelm,
@@ -66,6 +68,8 @@ func (pt Type) Name() string {
return "Composer"
case TypeConan:
return "Conan"
+ case TypeConda:
+ return "Conda"
case TypeContainer:
return "Container"
case TypeGeneric:
@@ -97,6 +101,8 @@ func (pt Type) SVGName() string {
return "gitea-composer"
case TypeConan:
return "gitea-conan"
+ case TypeConda:
+ return "gitea-conda"
case TypeContainer:
return "octicon-container"
case TypeGeneric:
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 200e0be6819..ac0205eb146 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -228,11 +228,6 @@ func (repo *Repository) IsBroken() bool {
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (repo *Repository) AfterLoad() {
- // FIXME: use models migration to solve all at once.
- if len(repo.DefaultBranch) == 0 {
- repo.DefaultBranch = setting.Repository.DefaultBranch
- }
-
repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index c1c8e71f53b..7f0e6e45643 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -75,7 +75,6 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy
if evt.Name != triggedEvent.Event() {
continue
}
-
if detectMatched(commit, triggedEvent, payload, evt) {
workflows[entry.Name()] = content
}
@@ -105,8 +104,9 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType
for cond, vals := range evt.Acts {
switch cond {
case "branches", "tags":
+ refShortName := git.RefName(pushPayload.Ref).ShortName()
for _, val := range vals {
- if glob.MustCompile(val, '/').Match(pushPayload.Ref) {
+ if glob.MustCompile(val, '/').Match(refShortName) {
matchTimes++
break
}
@@ -160,8 +160,9 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType
}
}
case "branches":
+ refShortName := git.RefName(prPayload.PullRequest.Base.Ref).ShortName()
for _, val := range vals {
- if glob.MustCompile(val, '/').Match(prPayload.PullRequest.Base.Ref) {
+ if glob.MustCompile(val, '/').Match(refShortName) {
matchTimes++
break
}
diff --git a/modules/charset/escape.go b/modules/charset/escape.go
index 3b1c2069779..5608836a451 100644
--- a/modules/charset/escape.go
+++ b/modules/charset/escape.go
@@ -44,7 +44,7 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
return streamer.escaped, err
}
-// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
+// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method.
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
bufRd := bufio.NewReader(reader)
outputStream := &HTMLStreamerWriter{Writer: writer}
@@ -65,10 +65,6 @@ func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale transl
}
break
}
- if err := streamer.SelfClosingTag("br"); err != nil {
- streamer.escaped.HasError = true
- return streamer.escaped, err
- }
}
return streamer.escaped, err
}
diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go
index 1247a81fea3..f0caa30eb82 100644
--- a/modules/httpcache/httpcache.go
+++ b/modules/httpcache/httpcache.go
@@ -19,14 +19,16 @@ import (
func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
directives := make([]string, 0, 2+len(additionalDirectives))
+ // "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store"
+ // because browsers may restore some input fields after navigate-back / reload a page.
if setting.IsProd {
if maxAge == 0 {
- directives = append(directives, "no-store")
+ directives = append(directives, "max-age=0", "private", "must-revalidate")
} else {
directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
}
} else {
- directives = append(directives, "no-store")
+ directives = append(directives, "max-age=0", "private", "must-revalidate")
// to remind users they are using non-prod setting.
h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
diff --git a/modules/packages/conda/metadata.go b/modules/packages/conda/metadata.go
new file mode 100644
index 00000000000..02dbf313ba2
--- /dev/null
+++ b/modules/packages/conda/metadata.go
@@ -0,0 +1,243 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conda
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/bzip2"
+ "io"
+ "strings"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/validation"
+
+ "github.com/klauspost/compress/zstd"
+)
+
+var (
+ ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument}
+ ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument}
+ ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument}
+)
+
+const (
+ PropertyName = "conda.name"
+ PropertyChannel = "conda.channel"
+ PropertySubdir = "conda.subdir"
+ PropertyMetadata = "conda.metdata"
+)
+
+// Package represents a Conda package
+type Package struct {
+ Name string
+ Version string
+ Subdir string
+ VersionMetadata *VersionMetadata
+ FileMetadata *FileMetadata
+}
+
+// VersionMetadata represents the metadata of a Conda package
+type VersionMetadata struct {
+ Description string `json:"description,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ RepositoryURL string `json:"repository_url,omitempty"`
+ DocumentationURL string `json:"documentation_url,omitempty"`
+ License string `json:"license,omitempty"`
+ LicenseFamily string `json:"license_family,omitempty"`
+}
+
+// FileMetadata represents the metadata of a Conda package file
+type FileMetadata struct {
+ IsCondaPackage bool `json:"is_conda"`
+ Architecture string `json:"architecture,omitempty"`
+ NoArch string `json:"noarch,omitempty"`
+ Build string `json:"build,omitempty"`
+ BuildNumber int64 `json:"build_number,omitempty"`
+ Dependencies []string `json:"dependencies,omitempty"`
+ Platform string `json:"platform,omitempty"`
+ Timestamp int64 `json:"timestamp,omitempty"`
+}
+
+type index struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Architecture string `json:"arch"`
+ NoArch string `json:"noarch"`
+ Build string `json:"build"`
+ BuildNumber int64 `json:"build_number"`
+ Dependencies []string `json:"depends"`
+ License string `json:"license"`
+ LicenseFamily string `json:"license_family"`
+ Platform string `json:"platform"`
+ Subdir string `json:"subdir"`
+ Timestamp int64 `json:"timestamp"`
+}
+
+type about struct {
+ Description string `json:"description"`
+ Summary string `json:"summary"`
+ ProjectURL string `json:"home"`
+ RepositoryURL string `json:"dev_url"`
+ DocumentationURL string `json:"doc_url"`
+}
+
+type ReaderAndReaderAt interface {
+ io.Reader
+ io.ReaderAt
+}
+
+// ParsePackageBZ2 parses the Conda package file compressed with bzip2
+func ParsePackageBZ2(r io.Reader) (*Package, error) {
+ gzr := bzip2.NewReader(r)
+
+ return parsePackageTar(gzr)
+}
+
+// ParsePackageConda parses the Conda package file compressed with zip and zstd
+func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) {
+ zr, err := zip.NewReader(r, size)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, file := range zr.File {
+ if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") {
+ f, err := zr.Open(file.Name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ dec, err := zstd.NewReader(f)
+ if err != nil {
+ return nil, err
+ }
+ defer dec.Close()
+
+ p, err := parsePackageTar(dec)
+ if p != nil {
+ p.FileMetadata.IsCondaPackage = true
+ }
+ return p, err
+ }
+ }
+
+ return nil, ErrInvalidStructure
+}
+
+func parsePackageTar(r io.Reader) (*Package, error) {
+ var i *index
+ var a *about
+
+ tr := tar.NewReader(r)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if hdr.Typeflag != tar.TypeReg {
+ continue
+ }
+
+ if hdr.Name == "info/index.json" {
+ if err := json.NewDecoder(tr).Decode(&i); err != nil {
+ return nil, err
+ }
+
+ if !checkName(i.Name) {
+ return nil, ErrInvalidName
+ }
+
+ if !checkVersion(i.Version) {
+ return nil, ErrInvalidVersion
+ }
+
+ if a != nil {
+ break // stop loop if both files were found
+ }
+ } else if hdr.Name == "info/about.json" {
+ if err := json.NewDecoder(tr).Decode(&a); err != nil {
+ return nil, err
+ }
+
+ if !validation.IsValidURL(a.ProjectURL) {
+ a.ProjectURL = ""
+ }
+ if !validation.IsValidURL(a.RepositoryURL) {
+ a.RepositoryURL = ""
+ }
+ if !validation.IsValidURL(a.DocumentationURL) {
+ a.DocumentationURL = ""
+ }
+
+ if i != nil {
+ break // stop loop if both files were found
+ }
+ }
+ }
+
+ if i == nil {
+ return nil, ErrInvalidStructure
+ }
+ if a == nil {
+ a = &about{}
+ }
+
+ return &Package{
+ Name: i.Name,
+ Version: i.Version,
+ Subdir: i.Subdir,
+ VersionMetadata: &VersionMetadata{
+ License: i.License,
+ LicenseFamily: i.LicenseFamily,
+ Description: a.Description,
+ Summary: a.Summary,
+ ProjectURL: a.ProjectURL,
+ RepositoryURL: a.RepositoryURL,
+ DocumentationURL: a.DocumentationURL,
+ },
+ FileMetadata: &FileMetadata{
+ Architecture: i.Architecture,
+ NoArch: i.NoArch,
+ Build: i.Build,
+ BuildNumber: i.BuildNumber,
+ Dependencies: i.Dependencies,
+ Platform: i.Platform,
+ Timestamp: i.Timestamp,
+ },
+ }, nil
+}
+
+// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393
+func checkName(name string) bool {
+ if name == "" {
+ return false
+ }
+ if name != strings.ToLower(name) {
+ return false
+ }
+ return !checkBadCharacters(name, "!")
+}
+
+// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403
+func checkVersion(version string) bool {
+ if version == "" {
+ return false
+ }
+ return !checkBadCharacters(version, "-")
+}
+
+func checkBadCharacters(s, additional string) bool {
+ if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") {
+ return true
+ }
+ return strings.ContainsAny(s, additional)
+}
diff --git a/modules/packages/conda/metadata_test.go b/modules/packages/conda/metadata_test.go
new file mode 100644
index 00000000000..2038ca370ce
--- /dev/null
+++ b/modules/packages/conda/metadata_test.go
@@ -0,0 +1,150 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conda
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "bytes"
+ "io"
+ "testing"
+
+ "github.com/dsnet/compress/bzip2"
+ "github.com/klauspost/compress/zstd"
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ packageName = "gitea"
+ packageVersion = "1.0.1"
+ description = "Package Description"
+ projectURL = "https://gitea.io"
+ repositoryURL = "https://gitea.io/gitea/gitea"
+ documentationURL = "https://docs.gitea.io"
+)
+
+func TestParsePackage(t *testing.T) {
+ createArchive := func(files map[string][]byte) *bytes.Buffer {
+ var buf bytes.Buffer
+ tw := tar.NewWriter(&buf)
+ for filename, content := range files {
+ hdr := &tar.Header{
+ Name: filename,
+ Mode: 0o600,
+ Size: int64(len(content)),
+ }
+ tw.WriteHeader(hdr)
+ tw.Write(content)
+ }
+ tw.Close()
+ return &buf
+ }
+
+ t.Run("MissingIndexFile", func(t *testing.T) {
+ buf := createArchive(map[string][]byte{"dummy.txt": {}})
+
+ p, err := parsePackageTar(buf)
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrInvalidStructure)
+ })
+
+ t.Run("MissingAboutFile", func(t *testing.T) {
+ buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"1.0"}`)})
+
+ p, err := parsePackageTar(buf)
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "name", p.Name)
+ assert.Equal(t, "1.0", p.Version)
+ assert.Empty(t, p.VersionMetadata.ProjectURL)
+ })
+
+ t.Run("InvalidName", func(t *testing.T) {
+ for _, name := range []string{"", "name!", "nAMe"} {
+ buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"` + name + `","version":"1.0"}`)})
+
+ p, err := parsePackageTar(buf)
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrInvalidName)
+ }
+ })
+
+ t.Run("InvalidVersion", func(t *testing.T) {
+ for _, version := range []string{"", "1.0-2"} {
+ buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"` + version + `"}`)})
+
+ p, err := parsePackageTar(buf)
+ assert.Nil(t, p)
+ assert.ErrorIs(t, err, ErrInvalidVersion)
+ }
+ })
+
+ t.Run("Valid", func(t *testing.T) {
+ buf := createArchive(map[string][]byte{
+ "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"linux-64"}`),
+ "info/about.json": []byte(`{"description":"` + description + `","dev_url":"` + repositoryURL + `","doc_url":"` + documentationURL + `","home":"` + projectURL + `"}`),
+ })
+
+ p, err := parsePackageTar(buf)
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.Equal(t, "linux-64", p.Subdir)
+ assert.Equal(t, description, p.VersionMetadata.Description)
+ assert.Equal(t, projectURL, p.VersionMetadata.ProjectURL)
+ assert.Equal(t, repositoryURL, p.VersionMetadata.RepositoryURL)
+ assert.Equal(t, documentationURL, p.VersionMetadata.DocumentationURL)
+ })
+
+ t.Run(".tar.bz2", func(t *testing.T) {
+ tarArchive := createArchive(map[string][]byte{
+ "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
+ })
+
+ var buf bytes.Buffer
+ bw, _ := bzip2.NewWriter(&buf, nil)
+ io.Copy(bw, tarArchive)
+ bw.Close()
+
+ br := bytes.NewReader(buf.Bytes())
+
+ p, err := ParsePackageBZ2(br)
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.False(t, p.FileMetadata.IsCondaPackage)
+ })
+
+ t.Run(".conda", func(t *testing.T) {
+ tarArchive := createArchive(map[string][]byte{
+ "info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
+ })
+
+ var infoBuf bytes.Buffer
+ zsw, _ := zstd.NewWriter(&infoBuf)
+ io.Copy(zsw, tarArchive)
+ zsw.Close()
+
+ var buf bytes.Buffer
+ zpw := zip.NewWriter(&buf)
+ w, _ := zpw.Create("info-x.tar.zst")
+ w.Write(infoBuf.Bytes())
+ zpw.Close()
+
+ br := bytes.NewReader(buf.Bytes())
+
+ p, err := ParsePackageConda(br, int64(br.Len()))
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.True(t, p.FileMetadata.IsCondaPackage)
+ })
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index 120fbb5bda8..d0cd80aa035 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -27,6 +27,7 @@ var (
LimitTotalOwnerSize int64
LimitSizeComposer int64
LimitSizeConan int64
+ LimitSizeConda int64
LimitSizeContainer int64
LimitSizeGeneric int64
LimitSizeHelm int64
@@ -66,6 +67,7 @@ func newPackages() {
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
+ Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA")
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 55e76da96d9..db16d20d9e4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -518,6 +518,7 @@ organization_leave_success = You have successfully left the organization %s.
invalid_ssh_key = Cannot verify your SSH key: %s
invalid_gpg_key = Cannot verify your GPG key: %s
invalid_ssh_principal = Invalid principal: %s
+must_use_public_key = The key you provided is a private key. Please do not upload your private key anywhere. Use your public key instead.
unable_verify_ssh_key = "Cannot verify the SSH key; double-check it for mistakes."
auth_failed = Authentication failed: %v
@@ -3162,6 +3163,11 @@ conan.details.repository = Repository
conan.registry = Setup this registry from the command line:
conan.install = To install the package using Conan, run the following command:
conan.documentation = For more information on the Conan registry, see the documentation.
+conda.registry = Setup this registry as a Conda repository in your .condarc file:
+conda.install = To install the package using Conda, run the following command:
+conda.documentation = For more information on the Conda registry, see the documentation.
+conda.details.repository_site = Repository Site
+conda.details.documentation_site = Documentation Site
container.details.type = Image Type
container.details.platform = Platform
container.details.repository_site = Repository Site
diff --git a/public/img/svg/gitea-conda.svg b/public/img/svg/gitea-conda.svg
new file mode 100644
index 00000000000..cd4817adf2e
--- /dev/null
+++ b/public/img/svg/gitea-conda.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 78eb5e860be..7a07fea815e 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/routers/api/packages/conan"
+ "code.gitea.io/gitea/routers/api/packages/conda"
"code.gitea.io/gitea/routers/api/packages/container"
"code.gitea.io/gitea/routers/api/packages/generic"
"code.gitea.io/gitea/routers/api/packages/helm"
@@ -167,6 +168,43 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
})
})
}, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/conda", func() {
+ var (
+ downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`)
+ uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`)
+ )
+
+ r.Get("/*", func(ctx *context.Context) {
+ m := downloadPattern.FindStringSubmatch(ctx.Params("*"))
+ if len(m) == 0 {
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
+ ctx.SetParams("architecture", m[2])
+ ctx.SetParams("filename", m[3])
+
+ switch m[3] {
+ case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
+ conda.EnumeratePackages(ctx)
+ default:
+ conda.DownloadPackageFile(ctx)
+ }
+ })
+ r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
+ m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
+ if len(m) == 0 {
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
+ ctx.SetParams("filename", m[2])
+
+ conda.UploadPackageFile(ctx)
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
r.Group("/{packagename}/{packageversion}", func() {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
new file mode 100644
index 00000000000..2ff619fed4e
--- /dev/null
+++ b/routers/api/packages/conda/conda.go
@@ -0,0 +1,306 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conda
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ conda_model "code.gitea.io/gitea/models/packages/conda"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ conda_module "code.gitea.io/gitea/modules/packages/conda"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+
+ "github.com/dsnet/compress/bzip2"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ ctx.JSON(status, struct {
+ Reason string `json:"reason"`
+ Message string `json:"message"`
+ }{
+ Reason: http.StatusText(status),
+ Message: message,
+ })
+ })
+}
+
+func EnumeratePackages(ctx *context.Context) {
+ type Info struct {
+ Subdir string `json:"subdir"`
+ }
+
+ type PackageInfo struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ NoArch string `json:"noarch"`
+ Subdir string `json:"subdir"`
+ Timestamp int64 `json:"timestamp"`
+ Build string `json:"build"`
+ BuildNumber int64 `json:"build_number"`
+ Dependencies []string `json:"depends"`
+ License string `json:"license"`
+ LicenseFamily string `json:"license_family"`
+ HashMD5 string `json:"md5"`
+ HashSHA256 string `json:"sha256"`
+ Size int64 `json:"size"`
+ }
+
+ type RepoData struct {
+ Info Info `json:"info"`
+ Packages map[string]*PackageInfo `json:"packages"`
+ PackagesConda map[string]*PackageInfo `json:"packages.conda"`
+ Removed map[string]*PackageInfo `json:"removed"`
+ }
+
+ repoData := &RepoData{
+ Info: Info{
+ Subdir: ctx.Params("architecture"),
+ },
+ Packages: make(map[string]*PackageInfo),
+ PackagesConda: make(map[string]*PackageInfo),
+ Removed: make(map[string]*PackageInfo),
+ }
+
+ pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Channel: ctx.Params("channel"),
+ Subdir: repoData.Info.Subdir,
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pfs) == 0 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ pds := make(map[int64]*packages_model.PackageDescriptor)
+
+ for _, pf := range pfs {
+ pd, exists := pds[pf.VersionID]
+ if !exists {
+ pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pd, err = packages_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pds[pf.VersionID] = pd
+ }
+
+ var pfd *packages_model.PackageFileDescriptor
+ for _, d := range pd.Files {
+ if d.File.ID == pf.ID {
+ pfd = d
+ break
+ }
+ }
+
+ var fileMetadata *conda_module.FileMetadata
+ if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ versionMetadata := pd.Metadata.(*conda_module.VersionMetadata)
+
+ pi := &PackageInfo{
+ Name: pd.PackageProperties.GetByName(conda_module.PropertyName),
+ Version: pd.Version.Version,
+ NoArch: fileMetadata.NoArch,
+ Subdir: repoData.Info.Subdir,
+ Timestamp: fileMetadata.Timestamp,
+ Build: fileMetadata.Build,
+ BuildNumber: fileMetadata.BuildNumber,
+ Dependencies: fileMetadata.Dependencies,
+ License: versionMetadata.License,
+ LicenseFamily: versionMetadata.LicenseFamily,
+ HashMD5: pfd.Blob.HashMD5,
+ HashSHA256: pfd.Blob.HashSHA256,
+ Size: pfd.Blob.Size,
+ }
+
+ if fileMetadata.IsCondaPackage {
+ repoData.PackagesConda[pfd.File.Name] = pi
+ } else {
+ repoData.Packages[pfd.File.Name] = pi
+ }
+ }
+
+ resp := ctx.Resp
+
+ var w io.Writer = resp
+
+ if strings.HasSuffix(ctx.Params("filename"), ".json") {
+ resp.Header().Set("Content-Type", "application/json")
+ } else {
+ resp.Header().Set("Content-Type", "application/x-bzip2")
+
+ zw, err := bzip2.NewWriter(w, nil)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer zw.Close()
+
+ w = zw
+ }
+
+ resp.WriteHeader(http.StatusOK)
+
+ if err := json.NewEncoder(w).Encode(repoData); err != nil {
+ log.Error("JSON encode: %v", err)
+ }
+}
+
+func UploadPackageFile(ctx *context.Context) {
+ upload, close, err := ctx.UploadStream()
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if close {
+ defer upload.Close()
+ }
+
+ buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ var pck *conda_module.Package
+ if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") {
+ pck, err = conda_module.ParsePackageBZ2(buf)
+ } else {
+ pck, err = conda_module.ParsePackageConda(buf, buf.Size())
+ }
+ if err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ fullName := pck.Name
+
+ channel := ctx.Params("channel")
+ if channel != "" {
+ fullName = channel + "/" + pck.Name
+ }
+
+ extension := ".tar.bz2"
+ if pck.FileMetadata.IsCondaPackage {
+ extension = ".conda"
+ }
+
+ fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ _, _, err = packages_service.CreatePackageOrAddFileToExisting(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeConda,
+ Name: fullName,
+ Version: pck.Version,
+ },
+ SemverCompatible: false,
+ Creator: ctx.Doer,
+ Metadata: pck.VersionMetadata,
+ PackageProperties: map[string]string{
+ conda_module.PropertyName: pck.Name,
+ conda_module.PropertyChannel: channel,
+ },
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension),
+ CompositeKey: pck.Subdir,
+ },
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
+ Properties: map[string]string{
+ conda_module.PropertySubdir: pck.Subdir,
+ conda_module.PropertyMetadata: string(fileMetadataRaw),
+ },
+ },
+ )
+ if err != nil {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusCreated)
+}
+
+func DownloadPackageFile(ctx *context.Context) {
+ pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Channel: ctx.Params("channel"),
+ Subdir: ctx.Params("architecture"),
+ Filename: ctx.Params("filename"),
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pfs) != 1 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ pf := pfs[0]
+
+ s, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index 6f9083ba327..5ffefc4862c 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
- // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
+ // enum: [composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
// - name: q
// in: query
// description: name filter
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index 57cf5f49e5b..46dc734d252 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -22,6 +22,7 @@ const (
func DefaultOrSystemWebhooks(ctx *context.Context) {
var err error
+ ctx.Data["Title"] = ctx.Tr("admin.hooks")
ctx.Data["PageIsAdminSystemHooks"] = true
ctx.Data["PageIsAdminDefaultHooks"] = true
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 54f503642bc..5204b5fd007 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -328,6 +328,14 @@ func NewRelease(ctx *context.Context) {
}
}
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ var err error
+ // Get assignees.
+ ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
+ if err != nil {
+ ctx.ServerError("GetAssignees", err)
+ return
+ }
+
upload.AddUploadContext(ctx, "release")
ctx.HTML(http.StatusOK, tplReleaseNew)
}
@@ -484,6 +492,13 @@ func EditRelease(ctx *context.Context) {
}
ctx.Data["attachments"] = rel.Attachments
+ // Get assignees.
+ ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, rel.Repo)
+ if err != nil {
+ ctx.ServerError("GetAssignees", err)
+ return
+ }
+
ctx.HTML(http.StatusOK, tplReleaseNew)
}
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index 02d253562f3..5f05435af79 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -60,7 +60,7 @@ const (
// SettingsCtxData is a middleware that sets all the general context data for the
// settings template.
func SettingsCtxData(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.options")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
@@ -894,7 +894,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
// Collaboration render a repository's collaboration page
func Collaboration(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
ctx.Data["PageIsSettingsCollaboration"] = true
users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
@@ -1134,7 +1134,7 @@ func GitHooksEditPost(ctx *context.Context) {
// DeployKeys render the deploy keys list of a repository page
func DeployKeys(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + " / " + ctx.Tr("secrets.secrets")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
@@ -1173,6 +1173,10 @@ func DeployKeysPost(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+ } else if err == asymkey_model.ErrKeyIsPrivate {
+ ctx.Data["HasError"] = true
+ ctx.Data["Err_Content"] = true
+ ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
} else {
ctx.Data["HasError"] = true
ctx.Data["Err_Content"] = true
diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go
index 31abde1ef68..a54565c1f15 100644
--- a/routers/web/repo/setting_protected_branch.go
+++ b/routers/web/repo/setting_protected_branch.go
@@ -31,7 +31,7 @@ const (
// ProtectedBranchRules render the page to protect the repository
func ProtectedBranchRules(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
ctx.Data["PageIsSettingsBranches"] = true
rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
@@ -46,7 +46,7 @@ func ProtectedBranchRules(ctx *context.Context) {
// SetDefaultBranchPost set default branch
func SetDefaultBranchPost(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.branches.update_default_branch")
ctx.Data["PageIsSettingsBranches"] = true
repo := ctx.Repo.Repository
diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go
index aa9edc73755..95bc6dfce7f 100644
--- a/routers/web/repo/tag.go
+++ b/routers/web/repo/tag.go
@@ -133,7 +133,7 @@ func DeleteProtectedTagPost(ctx *context.Context) {
}
func setTagsContext(ctx *context.Context) error {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
ctx.Data["PageIsSettingsTags"] = true
protectedTags, err := git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 95b3b4040d9..dbaa8fd351b 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -30,7 +30,7 @@ const (
// Account renders change user's password, user's email and user suicide page
func Account(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.account")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index 0aaf5920bc6..844d6fa166a 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -17,7 +17,7 @@ import (
// AdoptOrDeleteRepository adopts or deletes a repository
func AdoptOrDeleteRepository(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.adopt")
ctx.Data["PageIsSettingsRepos"] = true
allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowAdopt"] = allowAdopt
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index b66806ff2de..ac935e51bbb 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -21,7 +21,7 @@ const (
// Applications render manage access token page
func Applications(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsSettingsApplications"] = true
loadApplicationsData(ctx)
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index ec50eef9c18..6debf95bbce 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -23,7 +23,7 @@ const (
// Keys render user's SSH/GPG public keys page
func Keys(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
@@ -159,6 +159,8 @@ func KeysPost(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+ } else if err == asymkey_model.ErrKeyIsPrivate {
+ ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
}
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index af161952501..e01f3cdeea9 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -42,7 +42,7 @@ const (
// Profile render user's profile page
func Profile(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.profile")
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
@@ -219,7 +219,7 @@ func DeleteAvatar(ctx *context.Context) {
// Organization render all the organization of the user
func Organization(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.organization")
ctx.Data["PageIsSettingsOrganization"] = true
opts := organization.FindOrgOptions{
@@ -254,7 +254,7 @@ func Organization(ctx *context.Context) {
// Repos display a list of all repositories of the user
func Repos(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.repos")
ctx.Data["PageIsSettingsRepos"] = true
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
@@ -360,7 +360,7 @@ func Repos(ctx *context.Context) {
// Appearance render user's appearance settings
func Appearance(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true
var hiddenCommentTypes *big.Int
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index db6faaed6e1..6e6e7efb0b2 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -22,7 +22,7 @@ const (
// Security render change user's password page and 2FA
func Security(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["Title"] = ctx.Tr("settings.security")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.FormString("openid.return_to") != "" {
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 68ebba39176..16f13029f92 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -196,22 +196,39 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
}
// List all group memberships of a user
-func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
+func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
var ldapGroups []string
- groupFilter := fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
+ var searchFilter string
+
+ groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
+ if !ok {
+ return ldapGroups
+ }
+
+ groupDN, ok := source.sanitizedGroupDN(source.GroupDN)
+ if !ok {
+ return ldapGroups
+ }
+
+ if applyGroupFilter {
+ searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
+ } else {
+ searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
+ }
+
result, err := l.Search(ldap.NewSearchRequest(
- source.GroupDN,
+ groupDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
- groupFilter,
+ searchFilter,
[]string{},
nil,
))
if err != nil {
- log.Error("Failed group search using filter[%s]: %v", groupFilter, err)
+ log.Error("Failed group search in LDAP with filter [%s]: %v", searchFilter, err)
return ldapGroups
}
@@ -238,9 +255,7 @@ func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
}
// getMappedMemberships : returns the organizations and teams to modify the users membership
-func (source *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
- // get all LDAP group memberships for user
- usersLdapGroups := source.listLdapGroupMemberships(l, uid)
+func (source *Source) getMappedMemberships(usersLdapGroups []string, uid string) (map[string][]string, map[string][]string) {
// unmarshall LDAP group team map from configs
ldapGroupsToTeams := source.mapLdapGroupsToTeams()
membershipsToAdd := map[string][]string{}
@@ -260,6 +275,14 @@ func (source *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string
return membershipsToAdd, membershipsToRemove
}
+func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
+ if strings.ToLower(source.UserUID) == "dn" {
+ return entry.DN
+ }
+
+ return entry.GetAttributeValue(source.UserUID)
+}
+
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
@@ -375,58 +398,30 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
firstname := sr.Entries[0].GetAttributeValue(source.AttributeName)
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
- uid := sr.Entries[0].GetAttributeValue(source.UserUID)
- if source.UserUID == "dn" || source.UserUID == "DN" {
- uid = sr.Entries[0].DN
- }
+
+ teamsToAdd := make(map[string][]string)
+ teamsToRemove := make(map[string][]string)
// Check group membership
- if source.GroupsEnabled && source.GroupFilter != "" {
- groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
- if !ok {
- return nil
- }
- groupDN, ok := source.sanitizedGroupDN(source.GroupDN)
- if !ok {
+ if source.GroupsEnabled {
+ userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
+ usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
+
+ if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
return nil
}
- log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", source.GroupMemberUID, groupFilter, groupDN)
- groupSearch := ldap.NewSearchRequest(
- groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
- []string{source.GroupMemberUID},
- nil)
-
- srg, err := l.Search(groupSearch)
- if err != nil {
- log.Error("LDAP group search failed: %v", err)
- return nil
- } else if len(srg.Entries) < 1 {
- log.Error("LDAP group search failed: 0 entries")
- return nil
- }
-
- isMember := false
- Entries:
- for _, group := range srg.Entries {
- for _, member := range group.GetAttributeValues(source.GroupMemberUID) {
- if (source.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
- isMember = true
- break Entries
- }
- }
- }
-
- if !isMember {
- log.Error("LDAP group membership test failed")
- return nil
+ if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
+ teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
}
}
if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
}
+
isAdmin := checkAdmin(l, source, userDN)
+
var isRestricted bool
if !isAdmin {
isRestricted = checkRestricted(l, source, userDN)
@@ -436,12 +431,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
}
- teamsToAdd := make(map[string][]string)
- teamsToRemove := make(map[string][]string)
- if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- teamsToAdd, teamsToRemove = source.getMappedMemberships(l, uid)
- }
-
if !directBind && source.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
@@ -520,19 +509,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
return nil, err
}
- result := make([]*SearchResult, len(sr.Entries))
+ result := make([]*SearchResult, 0, len(sr.Entries))
- for i, v := range sr.Entries {
+ for _, v := range sr.Entries {
teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
- if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- userAttributeListedInGroup := v.GetAttributeValue(source.UserUID)
- if source.UserUID == "dn" || source.UserUID == "DN" {
- userAttributeListedInGroup = v.DN
+
+ if source.GroupsEnabled {
+ userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
+
+ if source.GroupFilter != "" {
+ usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
+ if len(usersLdapGroups) == 0 {
+ continue
+ }
+ }
+
+ if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
+ usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
+ teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
}
- teamsToAdd, teamsToRemove = source.getMappedMemberships(l, userAttributeListedInGroup)
}
- result[i] = &SearchResult{
+
+ user := &SearchResult{
Username: v.GetAttributeValue(source.AttributeUsername),
Name: v.GetAttributeValue(source.AttributeName),
Surname: v.GetAttributeValue(source.AttributeSurname),
@@ -541,16 +540,22 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
LdapTeamAdd: teamsToAdd,
LdapTeamRemove: teamsToRemove,
}
- if !result[i].IsAdmin {
- result[i].IsRestricted = checkRestricted(l, source, v.DN)
+
+ if !user.IsAdmin {
+ user.IsRestricted = checkRestricted(l, source, v.DN)
}
+
if isAttributeSSHPublicKeySet {
- result[i].SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
+ user.SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
}
+
if isAtributeAvatarSet {
- result[i].Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
+ user.Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
}
- result[i].LowerName = strings.ToLower(result[i].Username)
+
+ user.LowerName = strings.ToLower(user.Username)
+
+ result = append(result, user)
}
return result, nil
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index 734bb05dc69..e78e64ef7ed 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -15,7 +15,7 @@ import (
type PackageCleanupRuleForm struct {
ID int64
Enabled bool
- Type string `binding:"Required;In(composer,conan,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
+ Type string `binding:"Required;In(composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
KeepPattern string `binding:"RegexPattern"`
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 754dfa71102..9e52cb14509 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -339,6 +339,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
typeSpecificSize = setting.Packages.LimitSizeComposer
case packages_model.TypeConan:
typeSpecificSize = setting.Packages.LimitSizeConan
+ case packages_model.TypeConda:
+ typeSpecificSize = setting.Packages.LimitSizeConda
case packages_model.TypeContainer:
typeSpecificSize = setting.Packages.LimitSizeContainer
case packages_model.TypeGeneric:
diff --git a/services/pull/pull.go b/services/pull/pull.go
index c983c4f3e78..317875d2112 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -298,7 +298,6 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
}
}
- pr.Issue.PullRequest = pr
notification.NotifyPullRequestSynchronized(ctx, doer, pr)
}
}
diff --git a/templates/admin/applications/list.tmpl b/templates/admin/applications/list.tmpl
index 6d627129df0..4da6cb04465 100644
--- a/templates/admin/applications/list.tmpl
+++ b/templates/admin/applications/list.tmpl
@@ -1,5 +1,5 @@
{{template "base/head" .}}
-