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" .}} -
+
{{template "admin/navbar" .}}
diff --git a/templates/admin/applications/oauth2_edit.tmpl b/templates/admin/applications/oauth2_edit.tmpl index 84d821eccac..20231c4b1ca 100644 --- a/templates/admin/applications/oauth2_edit.tmpl +++ b/templates/admin/applications/oauth2_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}} {{template "user/settings/applications_oauth2_edit_form" .}} diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index bf9d53152c2..91e4b1df525 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl index c43287ee1ae..afe814cc6cb 100644 --- a/templates/admin/auth/list.tmpl +++ b/templates/admin/auth/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 213c621b42f..ab84dfccaf9 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index c2ab31862d9..8f572c83961 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 80eea91210c..40f28068d7d 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index f5f6a86dc8a..091f5011f95 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 407eae9646c..0c018ff2939 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/hooks.tmpl b/templates/admin/hooks.tmpl index a23cff43426..26f92c70641 100644 --- a/templates/admin/hooks.tmpl +++ b/templates/admin/hooks.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index f11d071ea43..d53e9e18dcc 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 36752d47b26..a2c7ca2f6ae 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index 11dc23c60eb..9bf7a6268e5 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 4c96d2bf105..bb36ca1ae35 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl index 675ec417e40..19dd70da121 100644 --- a/templates/admin/queue.tmpl +++ b/templates/admin/queue.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 837802f0d0c..11216b8c863 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 0c27c80e930..ca0b4c3bb9a 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 91929deaa88..4e16036ae3a 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 9e0f1d89fd1..ef436c718a6 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 67a726eb3f6..88af2172b76 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 9fdf0dce93d..e5ca864cb77 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl index f4e46d11987..924a3e62748 100644 --- a/templates/explore/code.tmpl +++ b/templates/explore/code.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "code/searchform" .}} diff --git a/templates/explore/organizations.tmpl b/templates/explore/organizations.tmpl index 36267baf339..56c6ee56a07 100644 --- a/templates/explore/organizations.tmpl +++ b/templates/explore/organizations.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/search" .}} diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl index 457ad74115e..dfede2ffcc0 100644 --- a/templates/explore/repos.tmpl +++ b/templates/explore/repos.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/repo_search" .}} diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl index 5fb79c0dd02..336daea7c8a 100644 --- a/templates/explore/users.tmpl +++ b/templates/explore/users.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "explore/navbar" .}}
{{template "explore/search" .}} diff --git a/templates/home.tmpl b/templates/home.tmpl index 87192fa55b6..52ad1b12dfc 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/install.tmpl b/templates/install.tmpl index 0625f43cc4e..3d33dcbcb76 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+

diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index 26c432303ce..8da46fb325b 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index eea5a076db6..b3724e7a63f 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{avatar .Org 140 "org-avatar"}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index fceb0801341..b4f788e5230 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl index 544ed387423..1f113b28c84 100644 --- a/templates/org/projects/list.tmpl +++ b/templates/org/projects/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/list" .}}
diff --git a/templates/org/projects/new.tmpl b/templates/org/projects/new.tmpl index b3d6c6001e3..f550cdc828d 100644 --- a/templates/org/projects/new.tmpl +++ b/templates/org/projects/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/new" .}}
diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index 03327e25300..c2d8f015f10 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}} {{template "projects/view" .}}
diff --git a/templates/org/settings/applications.tmpl b/templates/org/settings/applications.tmpl index 8bdd99deb86..35736df591e 100644 --- a/templates/org/settings/applications.tmpl +++ b/templates/org/settings/applications.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/applications_oauth2_edit.tmpl b/templates/org/settings/applications_oauth2_edit.tmpl index 2c7fa842b3b..861651a15e7 100644 --- a/templates/org/settings/applications_oauth2_edit.tmpl +++ b/templates/org/settings/applications_oauth2_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}} {{template "user/settings/applications_oauth2_edit_form" .}} diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 8a38dcafe35..669e393e1de 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 081f5a28394..4685225f4c8 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/hooks.tmpl b/templates/org/settings/hooks.tmpl index cb0243bb1eb..3abbc62ecf8 100644 --- a/templates/org/settings/hooks.tmpl +++ b/templates/org/settings/hooks.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index a6b5405a428..5436bcba05c 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 1b12c1221b9..b52639fb19b 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages.tmpl b/templates/org/settings/packages.tmpl index bb5d95e1072..412a9813c21 100644 --- a/templates/org/settings/packages.tmpl +++ b/templates/org/settings/packages.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages_cleanup_rules_edit.tmpl b/templates/org/settings/packages_cleanup_rules_edit.tmpl index 8c3725f4d7e..195c21da0c5 100644 --- a/templates/org/settings/packages_cleanup_rules_edit.tmpl +++ b/templates/org/settings/packages_cleanup_rules_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/packages_cleanup_rules_preview.tmpl b/templates/org/settings/packages_cleanup_rules_preview.tmpl index e0e4652c367..771e6cb8f3d 100644 --- a/templates/org/settings/packages_cleanup_rules_preview.tmpl +++ b/templates/org/settings/packages_cleanup_rules_preview.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/settings/secrets.tmpl b/templates/org/settings/secrets.tmpl index 70add15f50f..909c16f4484 100644 --- a/templates/org/settings/secrets.tmpl +++ b/templates/org/settings/secrets.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl index a696d994980..ef365dee3a0 100644 --- a/templates/org/team/invite.tmpl +++ b/templates/org/team/invite.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "base/alert" .}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 1a58dc5339e..13bd6c5b5db 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index 005d7ce4e51..d9d3902fe1a 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 80bdf7b3dbf..0391a762faf 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 5399f3a5940..66ac4a42119 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "org/header" .}}
{{template "base/alert" .}} diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl new file mode 100644 index 00000000000..ecc26bce98e --- /dev/null +++ b/templates/package/content/conda.tmpl @@ -0,0 +1,30 @@ +{{if eq .PackageDescriptor.Package.Type "conda"}} +

{{.locale.Tr "packages.installation"}}

+
+
+
+ +
channel_alias: {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+channels:
+  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+default_channels:
+  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
+
+
+ + {{$channel := .PackageDescriptor.PackageProperties.GetByName "conda.channel"}} +
conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}
+
+
+ +
+
+
+ + {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Summary}} +

{{.locale.Tr "packages.about"}}

+
+ {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{else}}{{.PackageDescriptor.Metadata.Summary}}{{end}} +
+ {{end}} +{{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl new file mode 100644 index 00000000000..2201c803562 --- /dev/null +++ b/templates/package/metadata/conda.tmpl @@ -0,0 +1,6 @@ +{{if eq .PackageDescriptor.Package.Type "conda"}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.conda.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.locale.Tr "packages.conda.details.documentation_site"}}
{{end}} +{{end}} diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index d1475be593c..a2c4bd4c282 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}}
{{template "base/alert" .}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index a5b2a2ef68a..4611cdb8d84 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "user/overview/header" .}}
@@ -21,6 +21,7 @@
{{template "package/content/composer" .}} {{template "package/content/conan" .}} + {{template "package/content/conda" .}} {{template "package/content/container" .}} {{template "package/content/generic" .}} {{template "package/content/helm" .}} @@ -44,6 +45,7 @@
{{svg "octicon-download" 16 "mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/composer" .}} {{template "package/metadata/conan" .}} + {{template "package/metadata/conda" .}} {{template "package/metadata/container" .}} {{template "package/metadata/generic" .}} {{template "package/metadata/helm" .}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index 4abfe171ddd..e098f43fda8 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index ae2eaec6eaa..21a3350a75d 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -1,4 +1,4 @@ -
+
{{if .CanWriteProjects}}