diff --git a/.drone.yml b/.drone.yml index f9da8f9743..dc8423875a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -763,10 +763,16 @@ steps: image: woodpeckerci/plugin-s3:latest pull: always settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: "/gitea/${DRONE_BRANCH##release/v}" @@ -784,10 +790,16 @@ steps: - name: release-main image: woodpeckerci/plugin-s3:latest settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: /gitea/main @@ -886,10 +898,16 @@ steps: image: woodpeckerci/plugin-s3:latest pull: always settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: "/gitea/${DRONE_TAG##v}" diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 8789bd3f06..e78e7968ac 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -799,6 +799,16 @@ "path": "github.com/olivere/elastic/v7/uritemplates/LICENSE", "licenseText": "Copyright (c) 2013 Joshua Tacoma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/opencontainers/go-digest", + "path": "github.com/opencontainers/go-digest/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n https://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2019, 2020 OCI Contributors\n Copyright 2016 Docker, Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, + { + "name": "github.com/opencontainers/image-spec/specs-go", + "path": "github.com/opencontainers/image-spec/specs-go/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2016 The Linux Foundation.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "github.com/pierrec/lz4/v4", "path": "github.com/pierrec/lz4/v4/LICENSE", diff --git a/cmd/admin.go b/cmd/admin.go index 8f8c68f981..318c212d08 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -308,6 +308,11 @@ var ( Value: "false", Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints", }, + cli.StringFlag{ + Name: "custom-tenant-id", + Value: "", + Usage: "Use custom Tenant ID for OAuth endpoints", + }, cli.StringFlag{ Name: "custom-auth-url", Value: "", @@ -367,6 +372,15 @@ var ( Value: "", Usage: "Group Claim value for restricted users", }, + cli.StringFlag{ + Name: "group-team-map", + Value: "", + Usage: "JSON mapping between groups and org teams", + }, + cli.BoolFlag{ + Name: "group-team-map-removal", + Usage: "Activate automatic team membership removal depending on groups", + }, } microcmdAuthUpdateOauth = cli.Command{ @@ -578,12 +592,16 @@ func runCreateUser(c *cli.Context) error { restricted = util.OptionalBoolOf(c.Bool("restricted")) } + // default user visibility in app.ini + visibility := setting.Service.DefaultUserVisibilityMode + u := &user_model.User{ Name: username, Email: c.String("email"), Passwd: password, IsAdmin: c.Bool("admin"), MustChangePassword: changePassword, + Visibility: visibility, } overwriteDefault := &user_model.CreateUserOverwriteOptions{ @@ -825,6 +843,7 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { AuthURL: c.String("custom-auth-url"), ProfileURL: c.String("custom-profile-url"), EmailURL: c.String("custom-email-url"), + Tenant: c.String("custom-tenant-id"), } } else { customURLMapping = nil @@ -843,6 +862,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { GroupClaimName: c.String("group-claim-name"), AdminGroup: c.String("admin-group"), RestrictedGroup: c.String("restricted-group"), + GroupTeamMap: c.String("group-team-map"), + GroupTeamMapRemoval: c.Bool("group-team-map-removal"), } } @@ -925,6 +946,12 @@ func runUpdateOauth(c *cli.Context) error { if c.IsSet("restricted-group") { oAuth2Config.RestrictedGroup = c.String("restricted-group") } + if c.IsSet("group-team-map") { + oAuth2Config.GroupTeamMap = c.String("group-team-map") + } + if c.IsSet("group-team-map-removal") { + oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") + } // update custom URL mapping customURLMapping := &oauth2.CustomURLMapping{} @@ -934,6 +961,7 @@ func runUpdateOauth(c *cli.Context) error { customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL + customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant } if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") { customURLMapping.TokenURL = c.String("custom-token-url") @@ -951,6 +979,10 @@ func runUpdateOauth(c *cli.Context) error { customURLMapping.EmailURL = c.String("custom-email-url") } + if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") { + customURLMapping.Tenant = c.String("custom-tenant-id") + } + oAuth2Config.CustomURLMapping = customURLMapping source.Cfg = oAuth2Config diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 8a95220184..627d2cf85e 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -765,7 +765,7 @@ ROUTER = console ;; Enable this to require captcha validation for login ;REQUIRE_CAPTCHA_FOR_LOGIN = false ;; -;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha. +;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile. ;CAPTCHA_TYPE = image ;; ;; Change this to use recaptcha.net or other recaptcha service @@ -787,6 +787,10 @@ ROUTER = console ;MCAPTCHA_SECRET = ;MCAPTCHA_SITEKEY = ;; +;; Go to https://dash.cloudflare.com/?to=/:account/turnstile to sign up for a key +;CF_TURNSTILE_SITEKEY = +;CF_TURNSTILE_SECRET = +;; ;; Default value for KeepEmailPrivate ;; Each new user will get the value of this setting copied into their profile ;DEFAULT_KEEP_EMAIL_PRIVATE = false @@ -2454,6 +2458,10 @@ ROUTER = console ;LIMIT_TOTAL_OWNER_COUNT = -1 ;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_TOTAL_OWNER_SIZE = -1 +;; Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_CARGO = -1 +;; Maximum size of a Chef upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_CHEF = -1 ;; Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_COMPOSER = -1 ;; Maximum size of a Conan 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 67ca7a5166..04344b15dc 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -643,7 +643,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o - `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`. - `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`. -- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\] +- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\] - `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha. - `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha. - `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net. @@ -652,6 +652,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o - `MCAPTCHA_SECRET`: **""**: Go to your mCaptcha instance to get a secret for mCaptcha. - `MCAPTCHA_SITEKEY`: **""**: Go to your mCaptcha instance to get a sitekey for mCaptcha. - `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: Set the mCaptcha URL. +- `CF_TURNSTILE_SECRET` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a secret for cloudflare turnstile. +- `CF_TURNSTILE_SITEKEY` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a sitekey for cloudflare turnstile. - `DEFAULT_KEEP_EMAIL_PRIVATE`: **false**: By default set users to keep their email address private. - `DEFAULT_ALLOW_CREATE_ORGANIZATION`: **true**: Allow new users to create organizations by default. - `DEFAULT_USER_IS_RESTRICTED`: **false**: Give new users restricted permissions by default @@ -1211,6 +1213,8 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload` - `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maximum count of package versions a single owner can have (`-1` means no limits) - `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_CARGO`: **-1**: Maximum size of a Cargo upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +- `LIMIT_SIZE_CHEF`: **-1**: Maximum size of a Chef upload (`-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`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index f10b6258c8..2598f16a14 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -147,6 +147,17 @@ menu: - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。 - `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。 - `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`。 +- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\],人机验证类型,分别表示图片认证、 recaptcha 、 hcaptcha 、mcaptcha 、和 cloudlfare 的 turnstile。 +- `RECAPTCHA_SECRET`: **""**: recaptcha 服务的密钥,可在 https://www.google.com/recaptcha/admin 获取。 +- `RECAPTCHA_SITEKEY`: **""**: recaptcha 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。 +- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: 设置 recaptcha 的 url 。 +- `HCAPTCHA_SECRET`: **""**: hcaptcha 服务的密钥,可在 https://www.hcaptcha.com/ 获取。 +- `HCAPTCHA_SITEKEY`: **""**: hcaptcha 服务的网站密钥,可在 https://www.hcaptcha.com/ 获取。 +- `MCAPTCHA_SECRET`: **""**: mCaptcha 服务的密钥。 +- `MCAPTCHA_SITEKEY`: **""**: mCaptcha 服务的网站密钥。 +- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: 设置 remCaptchacaptcha 的 url 。 +- `CF_TURNSTILE_SECRET` **""**: cloudlfare turnstile 服务的密钥,可在 https://dash.cloudflare.com/?to=/:account/turnstile 获取。 +- `CF_TURNSTILE_SITEKEY` **""**: cloudlfare turnstile 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。 ### Service - Expore (`service.explore`) diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index 2fcf0be156..c8327de7fe 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -449,3 +449,14 @@ It is highly recommended to back-up your database before running these commands. If you are using Cloudflare, turn off the auto-minify option in the dashboard. `Speed` -> `Optimization` -> Uncheck `HTML` within the `Auto-Minify` settings. + +## How to adopt repositories from disk + +- Add your (bare) repositories to the correct spot for your configuration (`repository.ROOT`), ensuring they are in the correct layout `/[user]/[repo].git`. + - **Note:** the directory names must be lowercase. + - You can also check `/admin/config` for the repository root path. +- Ensure that the user/org exists that you want to adopt repositories for. +- As an admin, go to `/admin/repos/unadopted` and search. + - Users can also be given similar permissions via config [`ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`]({{< relref "doc/advanced/config-cheat-sheet.en-us.md#repository" >}}). +- If the above steps are done correctly, you should be able to select repositories to adopt. + - If no repositories are found, enable [debug logging]({{< relref "doc/advanced/config-cheat-sheet.en-us.md#repository" >}}) to check for any specific errors. diff --git a/docs/content/doc/installation/with-docker-rootless.en-us.md b/docs/content/doc/installation/with-docker-rootless.en-us.md index 028d81ab91..7e1a0e6fe0 100644 --- a/docs/content/doc/installation/with-docker-rootless.en-us.md +++ b/docs/content/doc/installation/with-docker-rootless.en-us.md @@ -19,7 +19,7 @@ Gitea provides automatically updated Docker images within its Docker Hub organiz possible to always use the latest stable tag or to use another service that handles updating Docker images. -The rootless image use Gitea internal SSH to provide Git protocol and doesn't support OpenSSH. +The rootless image uses Gitea internal SSH to provide Git protocol and doesn't support OpenSSH. This reference setup guides users through the setup based on `docker-compose`, but the installation of `docker-compose` is out of scope of this documentation. To install `docker-compose` itself, follow diff --git a/docs/content/doc/packages/cargo.en-us.md b/docs/content/doc/packages/cargo.en-us.md new file mode 100644 index 0000000000..1f90d939d1 --- /dev/null +++ b/docs/content/doc/packages/cargo.en-us.md @@ -0,0 +1,109 @@ +--- +date: "2022-11-20T00:00:00+00:00" +title: "Cargo Packages Repository" +slug: "packages/cargo" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Cargo" + weight: 5 + identifier: "cargo" +--- + +# Cargo Packages Repository + +Publish [Cargo](https://doc.rust-lang.org/stable/cargo/) packages for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Cargo package registry, you need [Rust and Cargo](https://www.rust-lang.org/tools/install). + +Cargo stores informations about the available packages in a package index stored in a git repository. +This repository is needed to work with the registry. +The following section describes how to create it. + +## Index Repository + +Cargo stores informations about the available packages in a package index stored in a git repository. +In Gitea this repository has the special name `_cargo-index`. +After a package was uploaded, its metadata is automatically written to the index. +The content of this repository should not be manually modified. + +The user or organization package settings page allows to create the index repository along with the configuration file. +If needed this action will rewrite the configuration file. +This can be useful if for example the Gitea instance domain was changed. + +If the case arises where the packages stored in Gitea and the information in the index repository are out of sync, the settings page allows to rebuild the index repository. +This action iterates all packages in the registry and writes their information to the index. +If there are lot of packages this process may take some time. + +## Configuring the package registry + +To register the package registry the Cargo configuration must be updated. +Add the following text to the configuration file located in the current users home directory (for example `~/.cargo/config.toml`): + +``` +[registry] +default = "gitea" + +[registries.gitea] +index = "https://gitea.example.com/{owner}/_cargo-index.git" + +[net] +git-fetch-with-cli = true +``` + +| Parameter | Description | +| --------- | ----------- | +| `owner` | The owner of the package. | + +If the registry is private or you want to publish new packages, you have to configure your credentials. +Add the credentials section to the credentials file located in the current users home directory (for example `~/.cargo/credentials.toml`): + +``` +[registries.gitea] +token = "Bearer {token}" +``` + +| Parameter | Description | +| --------- | ----------- | +| `token` | Your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) | + +## Publish a package + +Publish a package by running the following command in your project: + +```shell +cargo publish +``` + +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 the following command: + +```shell +cargo add {package_name} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `package_name` | The package name. | + +## Supported commands + +``` +cargo publish +cargo add +cargo install +cargo yank +cargo unyank +cargo search +``` diff --git a/docs/content/doc/packages/chef.en-us.md b/docs/content/doc/packages/chef.en-us.md new file mode 100644 index 0000000000..ecc774d792 --- /dev/null +++ b/docs/content/doc/packages/chef.en-us.md @@ -0,0 +1,96 @@ +--- +date: "2023-01-20T00:00:00+00:00" +title: "Chef Packages Repository" +slug: "packages/chef" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Chef" + weight: 5 + identifier: "chef" +--- + +# Chef Packages Repository + +Publish [Chef](https://chef.io/) cookbooks for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Chef package registry, you have to use [`knife`](https://docs.chef.io/workstation/knife/). + +## Authentication + +The Chef package registry does not use an username:password authentication but signed requests with a private:public key pair. +Visit the package owner settings page to create the necessary key pair. +Only the public key is stored inside Gitea. if you loose access to the private key you must re-generate the key pair. +[Configure `knife`](https://docs.chef.io/workstation/knife_setup/) to use the downloaded private key with your Gitea username as `client_name`. + +## Configure the package registry + +To [configure `knife`](https://docs.chef.io/workstation/knife_setup/) to use the Gitea package registry add the url to the `~/.chef/config.rb` file. + +``` +knife[:supermarket_site] = 'https://gitea.example.com/api/packages/{owner}/chef' +``` + +| Parameter | Description | +| --------- | ----------- | +| `owner` | The owner of the package. | + +## Publish a package + +To publish a Chef package execute the following command: + +```shell +knife supermarket share {package_name} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `package_name` | The package name. | + +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 the following command: + +```shell +knife supermarket install {package_name} +``` + +Optional you can specify the package version: + +```shell +knife supermarket install {package_name} {package_version} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `package_name` | The package name. | +| `package_version` | The package version. | + +## Delete a package + +If you want to remove a package from the registry, execute the following command: + +```shell +knife supermarket unshare {package_name} +``` + +Optional you can specify the package version: + +```shell +knife supermarket unshare {package_name}/versions/{package_version} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `package_name` | The package name. | +| `package_version` | The package version. | diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index 9a736c1e56..1199d9ede0 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -26,6 +26,8 @@ The following package managers are currently supported: | Name | Language | Package client | | ---- | -------- | -------------- | +| [Cargo]({{< relref "doc/packages/cargo.en-us.md" >}}) | Rust | `cargo` | +| [Chef]({{< relref "doc/packages/chef.en-us.md" >}}) | - | `knife` | | [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` | diff --git a/docs/content/doc/upgrade/from-gogs.en-us.md b/docs/content/doc/upgrade/from-gogs.en-us.md index a10fffdcd6..f0bfe96050 100644 --- a/docs/content/doc/upgrade/from-gogs.en-us.md +++ b/docs/content/doc/upgrade/from-gogs.en-us.md @@ -85,8 +85,10 @@ Then repeat the procedure, but this time using the [latest release](https://dl.g ## Upgrading from a more recent version of Gogs -Upgrading from a more recent version of Gogs is also possible, but requires a bit more work. -See [#4286](https://github.com/go-gitea/gitea/issues/4286). +Upgrading from a more recent version of Gogs (up to `0.11.x`) may also be possible, but will require a bit more work. +See [#4286](https://github.com/go-gitea/gitea/issues/4286), which includes various Gogs `0.11.x` versions. + +Upgrading from Gogs `0.12.x` and above will be increasingly more difficult as the projects diverge further apart in configuration and schema. ## Troubleshooting diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index d9d397df31..9b861a9da3 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -124,6 +124,7 @@ Admin operations: - `--secret`: Client Secret. - `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider). - `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints. + - `--custom-tenant-id`: Use custom Tenant ID for OAuth endpoints. - `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub). - `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub). - `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub). @@ -136,6 +137,8 @@ Admin operations: - `--group-claim-name`: Claim name providing group names for this source. (Optional) - `--admin-group`: Group Claim value for administrator users. (Optional) - `--restricted-group`: Group Claim value for restricted users. (Optional) + - `--group-team-map`: JSON mapping between groups and org teams. (Optional) + - `--group-team-map-removal`: Activate automatic team membership removal depending on groups. (Optional) - Examples: - `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE` - `update-oauth`: @@ -147,6 +150,7 @@ Admin operations: - `--secret`: Client Secret. - `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider). - `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints. + - `--custom-tenant-id`: Use custom Tenant ID for OAuth endpoints. - `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub). - `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub). - `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub). diff --git a/go.mod b/go.mod index 7a9e5b8cd3..9a824f891f 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,8 @@ require ( github.com/niklasfasching/go-org v1.6.5 github.com/oliamb/cutter v0.2.2 github.com/olivere/elastic/v7 v7.0.32 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc2 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/prometheus/client_golang v1.14.0 diff --git a/go.sum b/go.sum index 8f122c8684..28ba3d8eed 100644 --- a/go.sum +++ b/go.sum @@ -1009,6 +1009,10 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= diff --git a/models/activities/action.go b/models/activities/action.go index 4baedbfe12..8e7492c008 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -223,18 +223,24 @@ func (a *Action) GetRepoAbsoluteLink() string { return setting.AppURL + url.PathEscape(a.GetRepoUserName()) + "/" + url.PathEscape(a.GetRepoName()) } -// GetCommentLink returns link to action comment. -func (a *Action) GetCommentLink() string { - return a.getCommentLink(db.DefaultContext) +// GetCommentHTMLURL returns link to action comment. +func (a *Action) GetCommentHTMLURL() string { + return a.getCommentHTMLURL(db.DefaultContext) } -func (a *Action) getCommentLink(ctx context.Context) string { +func (a *Action) loadComment(ctx context.Context) (err error) { + if a.CommentID == 0 || a.Comment != nil { + return nil + } + a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID) + return err +} + +func (a *Action) getCommentHTMLURL(ctx context.Context) string { if a == nil { return "#" } - if a.Comment == nil && a.CommentID != 0 { - a.Comment, _ = issues_model.GetCommentByID(ctx, a.CommentID) - } + _ = a.loadComment(ctx) if a.Comment != nil { return a.Comment.HTMLURL() } @@ -260,6 +266,41 @@ func (a *Action) getCommentLink(ctx context.Context) string { return issue.HTMLURL() } +// GetCommentLink returns link to action comment. +func (a *Action) GetCommentLink() string { + return a.getCommentLink(db.DefaultContext) +} + +func (a *Action) getCommentLink(ctx context.Context) string { + if a == nil { + return "#" + } + _ = a.loadComment(ctx) + if a.Comment != nil { + return a.Comment.Link() + } + if len(a.GetIssueInfos()) == 0 { + return "#" + } + // Return link to issue + issueIDString := a.GetIssueInfos()[0] + issueID, err := strconv.ParseInt(issueIDString, 10, 64) + if err != nil { + return "#" + } + + issue, err := issues_model.GetIssueByID(ctx, issueID) + if err != nil { + return "#" + } + + if err = issue.LoadRepo(ctx); err != nil { + return "#" + } + + return issue.Link() +} + // GetBranch returns the action's repository branch. func (a *Action) GetBranch() string { return strings.TrimPrefix(a.RefName, git.BranchPrefix) diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 29312bd482..f37e58f685 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -36,7 +36,7 @@ func TestAction_GetRepoLink(t *testing.T) { expected := path.Join(setting.AppSubURL, owner.Name, repo.Name) assert.Equal(t, expected, action.GetRepoLink()) assert.Equal(t, repo.HTMLURL(), action.GetRepoAbsoluteLink()) - assert.Equal(t, comment.HTMLURL(), action.GetCommentLink()) + assert.Equal(t, comment.HTMLURL(), action.GetCommentHTMLURL()) } func TestGetFeeds(t *testing.T) { diff --git a/models/activities/notification.go b/models/activities/notification.go index f153eb0589..75276a0443 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -459,6 +459,22 @@ func (n *Notification) HTMLURL() string { return "" } +// Link formats a relative URL-string to the notification +func (n *Notification) Link() string { + switch n.Source { + case NotificationSourceIssue, NotificationSourcePullRequest: + if n.Comment != nil { + return n.Comment.Link() + } + return n.Issue.Link() + case NotificationSourceCommit: + return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID) + case NotificationSourceRepository: + return n.Repository.Link() + } + return "" +} + // APIURL formats a URL-string to the notification func (n *Notification) APIURL() string { return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10) diff --git a/models/issues/comment.go b/models/issues/comment.go index 82c702a88c..2eefaffd84 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -393,21 +393,40 @@ func (c *Comment) HTMLURL() string { log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } + return c.Issue.HTMLURL() + c.hashLink() +} + +// Link formats a relative URL-string to the issue-comment +func (c *Comment) Link() string { + err := c.LoadIssue(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("LoadIssue(%d): %v", c.IssueID, err) + return "" + } + err = c.Issue.LoadRepo(db.DefaultContext) + if err != nil { // Silently dropping errors :unamused: + log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) + return "" + } + return c.Issue.Link() + c.hashLink() +} + +func (c *Comment) hashLink() string { if c.Type == CommentTypeCode { if c.ReviewID == 0 { - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + return "/files#" + c.HashTag() } if c.Review == nil { if err := c.LoadReview(); err != nil { log.Warn("LoadReview(%d): %v", c.ReviewID, err) - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + return "/files#" + c.HashTag() } } if c.Review.Type <= ReviewTypePending { - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + return "/files#" + c.HashTag() } } - return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag()) + return "#" + c.HashTag() } // APIURL formats a API-string to the issue-comment @@ -710,8 +729,8 @@ func (c *Comment) UnsignedLine() uint64 { return uint64(c.Line) } -// CodeCommentURL returns the url to a comment in code -func (c *Comment) CodeCommentURL() string { +// CodeCommentLink returns the url to a comment in code +func (c *Comment) CodeCommentLink() string { err := c.LoadIssue(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) @@ -722,7 +741,7 @@ func (c *Comment) CodeCommentURL() string { log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } - return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) + return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag()) } // LoadPushCommits Load push commits diff --git a/models/issues/issue.go b/models/issues/issue.go index 806016b576..63e153d7b3 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -421,7 +421,7 @@ func (issue *Issue) HTMLURL() string { return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) } -// Link returns the Link URL to this issue. +// Link returns the issue's relative URL. func (issue *Issue) Link() string { var path string if issue.IsPull { diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index 21ee24210f..a1086f9e81 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -277,26 +277,26 @@ func CommentTypeIsRef(t CommentType) bool { return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef } -// RefCommentHTMLURL returns the HTML URL for the comment that created this reference -func (c *Comment) RefCommentHTMLURL() string { +// RefCommentLink returns the relative URL for the comment that created this reference +func (c *Comment) RefCommentLink() string { // Edge case for when the reference is inside the title or the description of the referring issue if c.RefCommentID == 0 { - return c.RefIssueHTMLURL() + return c.RefIssueLink() } if err := c.LoadRefComment(); err != nil { // Silently dropping errors :unamused: log.Error("LoadRefComment(%d): %v", c.RefCommentID, err) return "" } - return c.RefComment.HTMLURL() + return c.RefComment.Link() } -// RefIssueHTMLURL returns the HTML URL of the issue where this reference was created -func (c *Comment) RefIssueHTMLURL() string { +// RefIssueLink returns the relative URL of the issue where this reference was created +func (c *Comment) RefIssueLink() string { if err := c.LoadRefIssue(); err != nil { // Silently dropping errors :unamused: log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) return "" } - return c.RefIssue.HTMLURL() + return c.RefIssue.Link() } // RefIssueTitle returns the title of the issue where this reference was created diff --git a/models/issues/pull.go b/models/issues/pull.go index 044fb5fa04..3f8b0bc7ac 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -759,8 +759,8 @@ func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRep return prs, nil } -// GetBaseBranchHTMLURL returns the HTML URL of the base branch -func (pr *PullRequest) GetBaseBranchHTMLURL() string { +// GetBaseBranchLink returns the relative URL of the base branch +func (pr *PullRequest) GetBaseBranchLink() string { if err := pr.LoadBaseRepo(db.DefaultContext); err != nil { log.Error("LoadBaseRepo: %v", err) return "" @@ -768,11 +768,11 @@ func (pr *PullRequest) GetBaseBranchHTMLURL() string { if pr.BaseRepo == nil { return "" } - return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) + return pr.BaseRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) } -// GetHeadBranchHTMLURL returns the HTML URL of the head branch -func (pr *PullRequest) GetHeadBranchHTMLURL() string { +// GetHeadBranchLink returns the relative URL of the head branch +func (pr *PullRequest) GetHeadBranchLink() string { if pr.Flow == PullRequestFlowAGit { return "" } @@ -784,7 +784,7 @@ func (pr *PullRequest) GetHeadBranchHTMLURL() string { if pr.HeadRepo == nil { return "" } - return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) + return pr.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) } // UpdateAllowEdits update if PR can be edited from maintainers diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 007c2fd903..f4efd916c8 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "xorm.io/xorm" ) @@ -175,7 +176,18 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error { } for _, pr := range prs { pr.Issue = set[pr.IssueID] - pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync + /* + Old code: + pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync + + It's worth panic because it's almost impossible to happen under normal use. + But in integration testing, an asynchronous task could read a database that has been reset. + So returning an error would make more sense, let the caller has a choice to ignore it. + */ + if pr.Issue == nil { + return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist) + } + pr.Issue.PullRequest = pr } return nil } diff --git a/models/organization/org.go b/models/organization/org.go index 05eaead60b..852facf704 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) { return CanCreateOrgRepo(db.DefaultContext, org.ID, uid) } -func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) { +// GetTeam returns named team of organization. +func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) { return GetTeam(ctx, org.ID, name) } -// GetTeam returns named team of organization. -func (org *Organization) GetTeam(name string) (*Team, error) { - return org.getTeam(db.DefaultContext, name) -} - -func (org *Organization) getOwnerTeam(ctx context.Context) (*Team, error) { - return org.getTeam(ctx, OwnerTeamName) -} - // GetOwnerTeam returns owner team of organization. -func (org *Organization) GetOwnerTeam() (*Team, error) { - return org.getOwnerTeam(db.DefaultContext) +func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) { + return org.GetTeam(ctx, OwnerTeamName) } // FindOrgTeams returns all teams of a given organization @@ -342,7 +334,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) { } // GetOrgByName returns organization by given name. -func GetOrgByName(name string) (*Organization, error) { +func GetOrgByName(ctx context.Context, name string) (*Organization, error) { if len(name) == 0 { return nil, ErrOrgNotExist{0, name} } @@ -350,7 +342,7 @@ func GetOrgByName(name string) (*Organization, error) { LowerName: strings.ToLower(name), Type: user_model.UserTypeOrganization, } - has, err := db.GetEngine(db.DefaultContext).Get(u) + has, err := db.GetEngine(ctx).Get(u) if err != nil { return nil, err } else if !has { diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 0a38365924..cfa304d7b2 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -61,28 +61,28 @@ func TestUser_IsOrgMember(t *testing.T) { func TestUser_GetTeam(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - team, err := org.GetTeam("team1") + team, err := org.GetTeam(db.DefaultContext, "team1") assert.NoError(t, err) assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, "team1", team.LowerName) - _, err = org.GetTeam("does not exist") + _, err = org.GetTeam(db.DefaultContext, "does not exist") assert.True(t, organization.IsErrTeamNotExist(err)) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) - _, err = nonOrg.GetTeam("team") + _, err = nonOrg.GetTeam(db.DefaultContext, "team") assert.True(t, organization.IsErrTeamNotExist(err)) } func TestUser_GetOwnerTeam(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - team, err := org.GetOwnerTeam() + team, err := org.GetOwnerTeam(db.DefaultContext) assert.NoError(t, err) assert.Equal(t, org.ID, team.OrgID) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) - _, err = nonOrg.GetOwnerTeam() + _, err = nonOrg.GetOwnerTeam(db.DefaultContext) assert.True(t, organization.IsErrTeamNotExist(err)) } @@ -115,15 +115,15 @@ func TestUser_GetMembers(t *testing.T) { func TestGetOrgByName(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - org, err := organization.GetOrgByName("user3") + org, err := organization.GetOrgByName(db.DefaultContext, "user3") assert.NoError(t, err) assert.EqualValues(t, 3, org.ID) assert.Equal(t, "user3", org.Name) - _, err = organization.GetOrgByName("user2") // user2 is an individual + _, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual assert.True(t, organization.IsErrOrgNotExist(err)) - _, err = organization.GetOrgByName("") // corner case + _, err = organization.GetOrgByName(db.DefaultContext, "") // corner case assert.True(t, organization.IsErrOrgNotExist(err)) } diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 3b36ee2266..f4be21e74e 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -11,6 +11,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/packages/cargo" + "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/packages/conan" "code.gitea.io/gitea/modules/packages/conda" @@ -63,7 +65,7 @@ type PackageFileDescriptor struct { // PackageWebLink returns the package web link func (pd *PackageDescriptor) PackageWebLink() string { - return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } // FullWebLink returns the package version web link @@ -129,6 +131,10 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc var metadata interface{} switch p.Type { + case TypeCargo: + metadata = &cargo.Metadata{} + case TypeChef: + metadata = &chef.Metadata{} case TypeComposer: metadata = &composer.Metadata{} case TypeConan: diff --git a/models/packages/package.go b/models/packages/package.go index 0015953d81..32f30fab9b 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -30,6 +30,8 @@ type Type string // List of supported packages const ( + TypeCargo Type = "cargo" + TypeChef Type = "chef" TypeComposer Type = "composer" TypeConan Type = "conan" TypeConda Type = "conda" @@ -46,6 +48,8 @@ const ( ) var TypeList = []Type{ + TypeCargo, + TypeChef, TypeComposer, TypeConan, TypeConda, @@ -64,6 +68,10 @@ var TypeList = []Type{ // Name gets the name of the package type func (pt Type) Name() string { switch pt { + case TypeCargo: + return "Cargo" + case TypeChef: + return "Chef" case TypeComposer: return "Composer" case TypeConan: @@ -97,6 +105,10 @@ func (pt Type) Name() string { // SVGName gets the name of the package type svg image func (pt Type) SVGName() string { switch pt { + case TypeCargo: + return "gitea-cargo" + case TypeChef: + return "gitea-chef" case TypeComposer: return "gitea-composer" case TypeConan: diff --git a/models/packages/package_property.go b/models/packages/package_property.go index 1b7f253d56..e03b12c9df 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -58,6 +58,12 @@ func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps) } +// UpdateProperty updates a property +func UpdateProperty(ctx context.Context, pp *PackageProperty) error { + _, err := db.GetEngine(ctx).ID(pp.ID).Update(pp) + return err +} + // DeleteAllProperties deletes all properties of a ref func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Delete(&PackageProperty{}) diff --git a/models/project/project.go b/models/project/project.go index 273823ac9d..9074fd0c15 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -116,6 +116,7 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +// Link returns the project's relative URL. func (p *Project) Link() string { if p.OwnerID > 0 { err := p.LoadOwner(db.DefaultContext) diff --git a/models/repo/release.go b/models/repo/release.go index 08b429f5e1..abf91bc4bb 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -130,6 +130,11 @@ func (r *Release) HTMLURL() string { return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) } +// Link the relative url for a release on the web UI. release must have attributes loaded +func (r *Release) Link() string { + return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) +} + // IsReleaseExist returns true if release with given tag name already exists. func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, error) { if len(tagName) == 0 { diff --git a/models/repo/repo.go b/models/repo/repo.go index 01b4c961d6..ccc4b8bb14 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -481,7 +481,7 @@ func (repo *Repository) RepoPath() string { return RepoPath(repo.OwnerName, repo.Name) } -// Link returns the repository link +// Link returns the repository relative url func (repo *Repository) Link() string { return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } diff --git a/modules/auth/common.go b/modules/auth/common.go new file mode 100644 index 0000000000..77361f6561 --- /dev/null +++ b/modules/auth/common.go @@ -0,0 +1,22 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" +) + +func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) { + groupTeamMapping := make(map[string]map[string][]string) + if raw == "" { + return groupTeamMapping, nil + } + err := json.Unmarshal([]byte(raw), &groupTeamMapping) + if err != nil { + log.Error("Failed to unmarshal group team mapping: %v", err) + return nil, err + } + return groupTeamMapping, nil +} diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 823b635137..1b956bf4ab 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -6,7 +6,6 @@ package charset import ( "fmt" "regexp" - "sort" "strings" "unicode" "unicode/utf8" @@ -20,12 +19,16 @@ import ( var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`) func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer { + allowedM := make(map[rune]bool, len(allowed)) + for _, v := range allowed { + allowedM[v] = true + } return &escapeStreamer{ escaped: &EscapeStatus{}, PassthroughHTMLStreamer: *NewPassthroughStreamer(next), locale: locale, ambiguousTables: AmbiguousTablesForLocale(locale), - allowed: allowed, + allowed: allowedM, } } @@ -34,7 +37,7 @@ type escapeStreamer struct { escaped *EscapeStatus locale translation.Locale ambiguousTables []*AmbiguousTable - allowed []rune + allowed map[rune]bool } func (e *escapeStreamer) EscapeStatus() *EscapeStatus { @@ -256,7 +259,7 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables runeCounts.numBrokenRunes++ case r == ' ' || r == '\t' || r == '\n': runeCounts.numBasicRunes++ - case e.isAllowed(r): + case e.allowed[r]: if r > 0x7e || r < 0x20 { types[i] = nonBasicASCIIRuneType runeCounts.numNonConfusingNonBasicRunes++ @@ -282,16 +285,3 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables } return types, confusables, runeCounts } - -func (e *escapeStreamer) isAllowed(r rune) bool { - if len(e.allowed) == 0 { - return false - } - if len(e.allowed) == 1 { - return e.allowed[0] == r - } - - return sort.Search(len(e.allowed), func(i int) bool { - return e.allowed[i] >= r - }) >= 0 -} diff --git a/modules/context/api.go b/modules/context/api.go index 3f52c54d4c..3f938948ae 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" - auth_service "code.gitea.io/gitea/services/auth" ) // APIContext is a specific context for API service @@ -215,35 +214,6 @@ func (ctx *APIContext) CheckForOTP() { } } -// APIAuth converts auth_service.Auth as a middleware -func APIAuth(authMethod auth_service.Method) func(*APIContext) { - return func(ctx *APIContext) { - // Get user from session if logged in. - var err error - ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) - if err != nil { - ctx.Error(http.StatusUnauthorized, "APIAuth", err) - return - } - - if ctx.Doer != nil { - if ctx.Locale.Language() != ctx.Doer.Language { - ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) - } - ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.Doer - ctx.Data["SignedUserID"] = ctx.Doer.ID - ctx.Data["SignedUserName"] = ctx.Doer.Name - ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - } - } -} - // APIContexter returns apicontext as middleware func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/modules/context/captcha.go b/modules/context/captcha.go index 735613504c..07232e9390 100644 --- a/modules/context/captcha.go +++ b/modules/context/captcha.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/mcaptcha" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/turnstile" "gitea.com/go-chi/captcha" ) @@ -47,12 +48,14 @@ func SetCaptchaData(ctx *Context) { ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL + ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey } const ( - gRecaptchaResponseField = "g-recaptcha-response" - hCaptchaResponseField = "h-captcha-response" - mCaptchaResponseField = "m-captcha-response" + gRecaptchaResponseField = "g-recaptcha-response" + hCaptchaResponseField = "h-captcha-response" + mCaptchaResponseField = "m-captcha-response" + cfTurnstileResponseField = "cf-turnstile-response" ) // VerifyCaptcha verifies Captcha data @@ -73,6 +76,8 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) { valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField)) case setting.MCaptcha: valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField)) + case setting.CfTurnstile: + valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField)) default: ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) return diff --git a/modules/context/context.go b/modules/context/context.go index 84f40ce063..a2088217ff 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -36,7 +36,6 @@ import ( "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" - "code.gitea.io/gitea/services/auth" "gitea.com/go-chi/cache" "gitea.com/go-chi/session" @@ -659,37 +658,6 @@ func getCsrfOpts() CsrfOptions { } } -// Auth converts auth.Auth as a middleware -func Auth(authMethod auth.Method) func(*Context) { - return func(ctx *Context) { - var err error - ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) - if err != nil { - log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err) - ctx.Error(http.StatusUnauthorized, "Verify") - return - } - if ctx.Doer != nil { - if ctx.Locale.Language() != ctx.Doer.Language { - ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) - } - ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth.BasicMethodName - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.Doer - ctx.Data["SignedUserID"] = ctx.Doer.ID - ctx.Data["SignedUserName"] = ctx.Doer.Name - ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - - // ensure the session uid is deleted - _ = ctx.Session.Delete("uid") - } - } -} - // Contexter initializes a classic context for a request. func Contexter(ctx context.Context) func(next http.Handler) http.Handler { _, rnd := templates.HTMLRenderer(ctx) diff --git a/modules/context/org.go b/modules/context/org.go index ff3a5ae7ec..0add7f2c0c 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { orgName := ctx.Params(":org") var err error - ctx.Org.Organization, err = organization.GetOrgByName(orgName) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) if err != nil { if organization.IsErrOrgNotExist(err) { redirectUserID, err := user_model.LookupUserRedirect(orgName) diff --git a/modules/git/blame.go b/modules/git/blame.go index 3b6e4c95db..ec88accb10 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -20,11 +20,12 @@ type BlamePart struct { // BlameReader returns part of file blame one by one type BlameReader struct { - cmd *Command - output io.WriteCloser - reader io.ReadCloser - done chan error - lastSha *string + cmd *Command + output io.WriteCloser + reader io.ReadCloser + bufferedReader *bufio.Reader + done chan error + lastSha *string } var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") @@ -33,8 +34,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") func (r *BlameReader) NextPart() (*BlamePart, error) { var blamePart *BlamePart - reader := bufio.NewReader(r.reader) - if r.lastSha != nil { blamePart = &BlamePart{*r.lastSha, make([]string, 0)} } @@ -44,7 +43,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { var err error for err != io.EOF { - line, isPrefix, err = reader.ReadLine() + line, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -66,7 +65,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { r.lastSha = &sha1 // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -81,7 +80,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -96,6 +95,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // Close BlameReader - don't run NextPart after invoking that func (r *BlameReader) Close() error { err := <-r.done + r.bufferedReader = nil _ = r.reader.Close() _ = r.output.Close() return err @@ -126,10 +126,13 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B done <- err }(cmd, repoPath, stdout, done) + bufferedReader := bufio.NewReader(reader) + return &BlameReader{ - cmd: cmd, - output: stdout, - reader: reader, - done: done, + cmd: cmd, + output: stdout, + reader: reader, + bufferedReader: bufferedReader, + done: done, }, nil } diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index a2c8fe8e75..1c0cd5c4aa 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -28,7 +28,7 @@ func TestReadingBlameOutput(t *testing.T) { }, { "f32b0a9dfd09a60f616f29158f772cedd89942d2", - []string{}, + []string{"", "Do not make any changes to this repo it is used for unit testing"}, }, } diff --git a/modules/git/repo.go b/modules/git/repo.go index e77a3a6ad8..233f7f20cf 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -163,10 +163,8 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op envs := os.Environ() u, err := url.Parse(from) - if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) { - if proxy.Match(u.Host) { - envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL())) - } + if err == nil { + envs = proxy.EnvWithProxy(u) } stderr := new(bytes.Buffer) diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index e7d5fb6806..2b34f117f7 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -135,8 +135,7 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error { c.env = append(c.env, "GIT_FLUSH=1") - // The empty "--" comes from #16773 , and it seems unnecessary because nothing else would be added later. - c.cmd.AddDynamicArguments(c.Attributes...).AddArguments("--") + c.cmd.AddDynamicArguments(c.Attributes...) var err error diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 17f8dd133f..94699c161c 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -4,7 +4,10 @@ package metrics import ( + "runtime" + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/modules/setting" "github.com/prometheus/client_golang/prometheus" ) @@ -17,6 +20,7 @@ type Collector struct { Accesses *prometheus.Desc Actions *prometheus.Desc Attachments *prometheus.Desc + BuildInfo *prometheus.Desc Comments *prometheus.Desc Follows *prometheus.Desc HookTasks *prometheus.Desc @@ -62,6 +66,16 @@ func NewCollector() Collector { "Number of Attachments", nil, nil, ), + BuildInfo: prometheus.NewDesc( + namespace+"build_info", + "Build information", + []string{ + "goarch", + "goos", + "goversion", + "version", + }, nil, + ), Comments: prometheus.NewDesc( namespace+"comments", "Number of Comments", @@ -195,6 +209,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) { ch <- c.Accesses ch <- c.Actions ch <- c.Attachments + ch <- c.BuildInfo ch <- c.Comments ch <- c.Follows ch <- c.HookTasks @@ -241,6 +256,15 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { prometheus.GaugeValue, float64(stats.Counter.Attachment), ) + ch <- prometheus.MustNewConstMetric( + c.BuildInfo, + prometheus.GaugeValue, + 1, + runtime.GOARCH, + runtime.GOOS, + runtime.Version(), + setting.AppVer, + ) ch <- prometheus.MustNewConstMetric( c.Comments, prometheus.GaugeValue, diff --git a/modules/migration/comment.go b/modules/migration/comment.go index f994e972ed..92ce30e302 100644 --- a/modules/migration/comment.go +++ b/modules/migration/comment.go @@ -8,8 +8,7 @@ import "time" // Commentable can be commented upon type Commentable interface { - GetLocalIndex() int64 - GetForeignIndex() int64 + Reviewable GetContext() DownloaderContext } diff --git a/modules/migration/issue.go b/modules/migration/issue.go index 7cb9f84b0d..3d1d1b4e0d 100644 --- a/modules/migration/issue.go +++ b/modules/migration/issue.go @@ -34,6 +34,15 @@ func (issue *Issue) GetExternalName() string { return issue.PosterName } // GetExternalID ExternalUserMigrated interface func (issue *Issue) GetExternalID() int64 { return issue.PosterID } -func (issue *Issue) GetLocalIndex() int64 { return issue.Number } -func (issue *Issue) GetForeignIndex() int64 { return issue.ForeignIndex } +func (issue *Issue) GetLocalIndex() int64 { return issue.Number } + +func (issue *Issue) GetForeignIndex() int64 { + // see the comment of Reviewable.GetForeignIndex + // if there is no ForeignIndex, then use LocalIndex + if issue.ForeignIndex == 0 { + return issue.Number + } + return issue.ForeignIndex +} + func (issue *Issue) GetContext() DownloaderContext { return issue.Context } diff --git a/modules/migration/review.go b/modules/migration/review.go index a420c130c7..79e821b2e1 100644 --- a/modules/migration/review.go +++ b/modules/migration/review.go @@ -8,6 +8,16 @@ import "time" // Reviewable can be reviewed type Reviewable interface { GetLocalIndex() int64 + + // GetForeignIndex presents the foreign index, which could be misused: + // For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it: + // * if site-A exports files by using its LocalIndex + // * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex + // * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex + // + // So the exporting/importing must be paired, but the meaning of them looks confusing then: + // * either site-A and site-B both use LocalIndex during dumping/restoring + // * or site-A and site-B both use ForeignIndex GetForeignIndex() int64 } @@ -37,7 +47,7 @@ type Review struct { // GetExternalName ExternalUserMigrated interface func (r *Review) GetExternalName() string { return r.ReviewerName } -// ExternalID ExternalUserMigrated interface +// GetExternalID ExternalUserMigrated interface func (r *Review) GetExternalID() int64 { return r.ReviewerID } // ReviewComment represents a review comment diff --git a/modules/packages/cargo/parser.go b/modules/packages/cargo/parser.go new file mode 100644 index 0000000000..36cd44df84 --- /dev/null +++ b/modules/packages/cargo/parser.go @@ -0,0 +1,169 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "encoding/binary" + "errors" + "io" + "regexp" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/validation" + + "github.com/hashicorp/go-version" +) + +const PropertyYanked = "cargo.yanked" + +var ( + ErrInvalidName = errors.New("package name is invalid") + ErrInvalidVersion = errors.New("package version is invalid") +) + +// Package represents a Cargo package +type Package struct { + Name string + Version string + Metadata *Metadata + Content io.Reader + ContentSize int64 +} + +// Metadata represents the metadata of a Cargo package +type Metadata struct { + Dependencies []*Dependency `json:"dependencies,omitempty"` + Features map[string][]string `json:"features,omitempty"` + Authors []string `json:"authors,omitempty"` + Description string `json:"description,omitempty"` + DocumentationURL string `json:"documentation_url,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Readme string `json:"readme,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Categories []string `json:"categories,omitempty"` + License string `json:"license,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Links string `json:"links,omitempty"` +} + +type Dependency struct { + Name string `json:"name"` + Req string `json:"req"` + Features []string `json:"features"` + Optional bool `json:"optional"` + DefaultFeatures bool `json:"default_features"` + Target *string `json:"target"` + Kind string `json:"kind"` + Registry *string `json:"registry"` + Package *string `json:"package"` +} + +var nameMatch = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9-_]{0,63}\z`) + +// ParsePackage reads the metadata and content of a package +func ParsePackage(r io.Reader) (*Package, error) { + var size uint32 + if err := binary.Read(r, binary.LittleEndian, &size); err != nil { + return nil, err + } + + p, err := parsePackage(io.LimitReader(r, int64(size))) + if err != nil { + return nil, err + } + + if err := binary.Read(r, binary.LittleEndian, &size); err != nil { + return nil, err + } + + p.Content = io.LimitReader(r, int64(size)) + p.ContentSize = int64(size) + + return p, nil +} + +func parsePackage(r io.Reader) (*Package, error) { + var meta struct { + Name string `json:"name"` + Vers string `json:"vers"` + Deps []struct { + Name string `json:"name"` + VersionReq string `json:"version_req"` + Features []string `json:"features"` + Optional bool `json:"optional"` + DefaultFeatures bool `json:"default_features"` + Target *string `json:"target"` + Kind string `json:"kind"` + Registry *string `json:"registry"` + ExplicitNameInToml string `json:"explicit_name_in_toml"` + } `json:"deps"` + Features map[string][]string `json:"features"` + Authors []string `json:"authors"` + Description string `json:"description"` + Documentation string `json:"documentation"` + Homepage string `json:"homepage"` + Readme string `json:"readme"` + ReadmeFile string `json:"readme_file"` + Keywords []string `json:"keywords"` + Categories []string `json:"categories"` + License string `json:"license"` + LicenseFile string `json:"license_file"` + Repository string `json:"repository"` + Links string `json:"links"` + } + if err := json.NewDecoder(r).Decode(&meta); err != nil { + return nil, err + } + + if !nameMatch.MatchString(meta.Name) { + return nil, ErrInvalidName + } + + if _, err := version.NewSemver(meta.Vers); err != nil { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(meta.Homepage) { + meta.Homepage = "" + } + if !validation.IsValidURL(meta.Documentation) { + meta.Documentation = "" + } + if !validation.IsValidURL(meta.Repository) { + meta.Repository = "" + } + + dependencies := make([]*Dependency, 0, len(meta.Deps)) + for _, dep := range meta.Deps { + dependencies = append(dependencies, &Dependency{ + Name: dep.Name, + Req: dep.VersionReq, + Features: dep.Features, + Optional: dep.Optional, + DefaultFeatures: dep.DefaultFeatures, + Target: dep.Target, + Kind: dep.Kind, + Registry: dep.Registry, + }) + } + + return &Package{ + Name: meta.Name, + Version: meta.Vers, + Metadata: &Metadata{ + Dependencies: dependencies, + Features: meta.Features, + Authors: meta.Authors, + Description: meta.Description, + DocumentationURL: meta.Documentation, + ProjectURL: meta.Homepage, + Readme: meta.Readme, + Keywords: meta.Keywords, + Categories: meta.Categories, + License: meta.License, + RepositoryURL: meta.Repository, + Links: meta.Links, + }, + }, nil +} diff --git a/modules/packages/cargo/parser_test.go b/modules/packages/cargo/parser_test.go new file mode 100644 index 0000000000..2230a5b499 --- /dev/null +++ b/modules/packages/cargo/parser_test.go @@ -0,0 +1,86 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "bytes" + "encoding/binary" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + description = "Package Description" + author = "KN4CK3R" + homepage = "https://gitea.io/" + license = "MIT" +) + +func TestParsePackage(t *testing.T) { + createPackage := func(name, version string) io.Reader { + metadata := `{ + "name":"` + name + `", + "vers":"` + version + `", + "description":"` + description + `", + "authors": ["` + author + `"], + "deps":[ + { + "name":"dep", + "version_req":"1.0" + } + ], + "homepage":"` + homepage + `", + "license":"` + license + `" +}` + + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, uint32(len(metadata))) + buf.WriteString(metadata) + binary.Write(&buf, binary.LittleEndian, uint32(4)) + buf.WriteString("test") + return &buf + } + + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{"", "0test", "-test", "_test", strings.Repeat("a", 65)} { + data := createPackage(name, "1.0.0") + + cp, err := ParsePackage(data) + assert.Nil(t, cp) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"", "1.", "-1.0", "1.0.0/1"} { + data := createPackage("test", version) + + cp, err := ParsePackage(data) + assert.Nil(t, cp) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage("test", "1.0.0") + + cp, err := ParsePackage(data) + assert.NotNil(t, cp) + assert.NoError(t, err) + + assert.Equal(t, "test", cp.Name) + assert.Equal(t, "1.0.0", cp.Version) + assert.Equal(t, description, cp.Metadata.Description) + assert.Equal(t, []string{author}, cp.Metadata.Authors) + assert.Len(t, cp.Metadata.Dependencies, 1) + assert.Equal(t, "dep", cp.Metadata.Dependencies[0].Name) + assert.Equal(t, homepage, cp.Metadata.ProjectURL) + assert.Equal(t, license, cp.Metadata.License) + content, _ := io.ReadAll(cp.Content) + assert.Equal(t, "test", string(content)) + }) +} diff --git a/modules/packages/chef/metadata.go b/modules/packages/chef/metadata.go new file mode 100644 index 0000000000..a1c91870c2 --- /dev/null +++ b/modules/packages/chef/metadata.go @@ -0,0 +1,134 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "archive/tar" + "compress/gzip" + "io" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" +) + +const ( + KeyBits = 4096 + SettingPublicPem = "chef.public_pem" +) + +var ( + ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + + namePattern = regexp.MustCompile(`\A\S+\z`) + versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`) +) + +// Package represents a Chef package +type Package struct { + Name string + Version string + Metadata *Metadata +} + +// Metadata represents the metadata of a Chef package +type Metadata struct { + Description string `json:"description,omitempty"` + LongDescription string `json:"long_description,omitempty"` + Author string `json:"author,omitempty"` + License string `json:"license,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Dependencies map[string]string `json:"dependencies,omitempty"` +} + +type chefMetadata struct { + Name string `json:"name"` + Description string `json:"description"` + LongDescription string `json:"long_description"` + Maintainer string `json:"maintainer"` + MaintainerEmail string `json:"maintainer_email"` + License string `json:"license"` + Platforms map[string]string `json:"platforms"` + Dependencies map[string]string `json:"dependencies"` + Providing map[string]string `json:"providing"` + Recipes map[string]string `json:"recipes"` + Version string `json:"version"` + SourceURL string `json:"source_url"` + IssuesURL string `json:"issues_url"` + Privacy bool `json:"privacy"` + ChefVersions [][]string `json:"chef_versions"` + Gems [][]string `json:"gems"` + EagerLoadLibraries bool `json:"eager_load_libraries"` +} + +// ParsePackage parses the Chef package file +func ParsePackage(r io.Reader) (*Package, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if strings.Count(hd.Name, "/") != 1 { + continue + } + + if hd.FileInfo().Name() == "metadata.json" { + return ParseChefMetadata(tr) + } + } + + return nil, ErrMissingMetadataFile +} + +// ParseChefMetadata parses a metadata.json file to retrieve the metadata of a Chef package +func ParseChefMetadata(r io.Reader) (*Package, error) { + var cm chefMetadata + if err := json.NewDecoder(r).Decode(&cm); err != nil { + return nil, err + } + + if !namePattern.MatchString(cm.Name) { + return nil, ErrInvalidName + } + + if !versionPattern.MatchString(cm.Version) { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(cm.SourceURL) { + cm.SourceURL = "" + } + + return &Package{ + Name: cm.Name, + Version: cm.Version, + Metadata: &Metadata{ + Description: cm.Description, + LongDescription: cm.LongDescription, + Author: cm.Maintainer, + License: cm.License, + RepositoryURL: cm.SourceURL, + Dependencies: cm.Dependencies, + }, + }, nil +} diff --git a/modules/packages/chef/metadata_test.go b/modules/packages/chef/metadata_test.go new file mode 100644 index 0000000000..6def4162a9 --- /dev/null +++ b/modules/packages/chef/metadata_test.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageAuthor = "KN4CK3R" + packageDescription = "Package Description" + packageRepositoryURL = "https://gitea.io/gitea/gitea" +) + +func TestParsePackage(t *testing.T) { + t.Run("MissingMetadataFile", func(t *testing.T) { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + tw.Close() + zw.Close() + + p, err := ParsePackage(&buf) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrMissingMetadataFile) + }) + + t.Run("Valid", func(t *testing.T) { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + + content := `{"name":"` + packageName + `","version":"` + packageVersion + `"}` + + hdr := &tar.Header{ + Name: packageName + "/metadata.json", + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write([]byte(content)) + + tw.Close() + zw.Close() + + p, err := ParsePackage(&buf) + assert.NoError(t, err) + assert.NotNil(t, p) + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.NotNil(t, p.Metadata) + }) +} + +func TestParseChefMetadata(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{" test", "test "} { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + name + `","version":"1.0.0"}`)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"1", "1.2.3.4", "1.0.0 "} { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"test","version":"` + version + `"}`)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("Valid", func(t *testing.T) { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + packageName + `","version":"` + packageVersion + `","description":"` + packageDescription + `","maintainer":"` + packageAuthor + `","source_url":"` + packageRepositoryURL + `"}`)) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.Metadata.Description) + assert.Equal(t, packageAuthor, p.Metadata.Author) + assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL) + }) +} diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index c3946f38f1..6f62ab6a54 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -10,8 +10,9 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/validation" + + oci "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -65,8 +66,8 @@ type Metadata struct { } // ParseImageConfig parses the metadata of an image config -func ParseImageConfig(mediaType oci.MediaType, r io.Reader) (*Metadata, error) { - if strings.EqualFold(string(mediaType), helm.ConfigMediaType) { +func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { + if strings.EqualFold(mt, helm.ConfigMediaType) { return parseHelmConfig(r) } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index f9ee478d63..5d8d3abfae 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -8,8 +8,8 @@ import ( "testing" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" + oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" ) @@ -23,7 +23,7 @@ func TestParseImageConfig(t *testing.T) { configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}` - metadata, err := ParseImageConfig(oci.MediaType(oci.MediaTypeImageManifest), strings.NewReader(configOCI)) + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) assert.NoError(t, err) assert.Equal(t, TypeOCI, metadata.Type) @@ -50,7 +50,7 @@ func TestParseImageConfig(t *testing.T) { configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}` - metadata, err = ParseImageConfig(oci.MediaType(helm.ConfigMediaType), strings.NewReader(configHelm)) + metadata, err = ParseImageConfig(helm.ConfigMediaType, strings.NewReader(configHelm)) assert.NoError(t, err) assert.Equal(t, TypeHelm, metadata.Type) diff --git a/modules/packages/container/oci/digest.go b/modules/packages/container/oci/digest.go deleted file mode 100644 index dd9cc0095c..0000000000 --- a/modules/packages/container/oci/digest.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "regexp" - "strings" -) - -var digestPattern = regexp.MustCompile(`\Asha256:[a-f0-9]{64}\z`) - -type Digest string - -// Validate checks if the digest has a valid SHA256 signature -func (d Digest) Validate() bool { - return digestPattern.MatchString(string(d)) -} - -func (d Digest) Hash() string { - p := strings.SplitN(string(d), ":", 2) - if len(p) != 2 { - return "" - } - return p[1] -} diff --git a/modules/packages/container/oci/mediatype.go b/modules/packages/container/oci/mediatype.go deleted file mode 100644 index f9c3907e17..0000000000 --- a/modules/packages/container/oci/mediatype.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "strings" -) - -const ( - MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" - MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" - MediaTypeDockerManifest = "application/vnd.docker.distribution.manifest.v2+json" - MediaTypeDockerManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" -) - -type MediaType string - -// IsValid tests if the media type is in the OCI or Docker namespace -func (m MediaType) IsValid() bool { - s := string(m) - return strings.HasPrefix(s, "application/vnd.docker.") || strings.HasPrefix(s, "application/vnd.oci.") -} - -// IsImageManifest tests if the media type is an image manifest -func (m MediaType) IsImageManifest() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifest) || strings.EqualFold(s, MediaTypeImageManifest) -} - -// IsImageIndex tests if the media type is an image index -func (m MediaType) IsImageIndex() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifestList) || strings.EqualFold(s, MediaTypeImageIndex) -} diff --git a/modules/packages/container/oci/oci.go b/modules/packages/container/oci/oci.go deleted file mode 100644 index 570d2e92c2..0000000000 --- a/modules/packages/container/oci/oci.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "time" -) - -// https://github.com/opencontainers/image-spec/tree/main/specs-go/v1 - -// ImageConfig defines the execution parameters which should be used as a base when running a container using an image. -type ImageConfig struct { - // User defines the username or UID which the process in the container should run as. - User string `json:"User,omitempty"` - - // ExposedPorts a set of ports to expose from a container running this image. - ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` - - // Env is a list of environment variables to be used in a container. - Env []string `json:"Env,omitempty"` - - // Entrypoint defines a list of arguments to use as the command to execute when the container starts. - Entrypoint []string `json:"Entrypoint,omitempty"` - - // Cmd defines the default arguments to the entrypoint of the container. - Cmd []string `json:"Cmd,omitempty"` - - // Volumes is a set of directories describing where the process is likely write data specific to a container instance. - Volumes map[string]struct{} `json:"Volumes,omitempty"` - - // WorkingDir sets the current working directory of the entrypoint process in the container. - WorkingDir string `json:"WorkingDir,omitempty"` - - // Labels contains arbitrary metadata for the container. - Labels map[string]string `json:"Labels,omitempty"` - - // StopSignal contains the system call signal that will be sent to the container to exit. - StopSignal string `json:"StopSignal,omitempty"` -} - -// RootFS describes a layer content addresses -type RootFS struct { - // Type is the type of the rootfs. - Type string `json:"type"` - - // DiffIDs is an array of layer content hashes, in order from bottom-most to top-most. - DiffIDs []string `json:"diff_ids"` -} - -// History describes the history of a layer. -type History struct { - // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // CreatedBy is the command which created the layer. - CreatedBy string `json:"created_by,omitempty"` - - // Author is the author of the build point. - Author string `json:"author,omitempty"` - - // Comment is a custom message set when creating the layer. - Comment string `json:"comment,omitempty"` - - // EmptyLayer is used to mark if the history item created a filesystem diff. - EmptyLayer bool `json:"empty_layer,omitempty"` -} - -// Image is the JSON structure which describes some basic information about the image. -// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. -type Image struct { - // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. - Author string `json:"author,omitempty"` - - // Architecture is the CPU architecture which the binaries in this image are built to run on. - Architecture string `json:"architecture"` - - // Variant is the variant of the specified CPU architecture which image binaries are intended to run on. - Variant string `json:"variant,omitempty"` - - // OS is the name of the operating system which the image is built to run on. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Config defines the execution parameters which should be used as a base when running a container using the image. - Config ImageConfig `json:"config,omitempty"` - - // RootFS references the layer content addresses used by the image. - RootFS RootFS `json:"rootfs"` - - // History describes the history of each layer. - History []History `json:"history,omitempty"` -} - -// Descriptor describes the disposition of targeted content. -// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype -// when marshalled to JSON. -type Descriptor struct { - // MediaType is the media type of the object this schema refers to. - MediaType MediaType `json:"mediaType,omitempty"` - - // Digest is the digest of the targeted content. - Digest Digest `json:"digest"` - - // Size specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // URLs specifies a list of URLs from which this object MAY be downloaded - URLs []string `json:"urls,omitempty"` - - // Annotations contains arbitrary metadata relating to the targeted content. - Annotations map[string]string `json:"annotations,omitempty"` - - // Data is an embedding of the targeted content. This is encoded as a base64 - // string when marshalled to JSON (automatically, by encoding/json). If - // present, Data can be used directly to avoid fetching the targeted content. - Data []byte `json:"data,omitempty"` - - // Platform describes the platform which the image in the manifest runs on. - // - // This should only be used when referring to a manifest. - Platform *Platform `json:"platform,omitempty"` -} - -// Platform describes the platform which the image in the manifest runs on. -type Platform struct { - // Architecture field specifies the CPU architecture, for example - // `amd64` or `ppc64`. - Architecture string `json:"architecture"` - - // OS specifies the operating system, for example `linux` or `windows`. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Variant is an optional field specifying a variant of the CPU, for - // example `v7` to specify ARMv7 when architecture is `arm`. - Variant string `json:"variant,omitempty"` -} - -type SchemaMediaBase struct { - // SchemaVersion is the image manifest schema that this image follows - SchemaVersion int `json:"schemaVersion"` - - // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` - MediaType MediaType `json:"mediaType,omitempty"` -} - -// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON. -type Manifest struct { - SchemaMediaBase - - // Config references a configuration object for a container, by digest. - // The referenced configuration object is a JSON blob that the runtime uses to set up the container. - Config Descriptor `json:"config"` - - // Layers is an indexed list of layers referenced by the manifest. - Layers []Descriptor `json:"layers"` - - // Annotations contains arbitrary metadata for the image manifest. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// Index references manifests for various platforms. -// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON. -type Index struct { - SchemaMediaBase - - // Manifests references platform specific manifests. - Manifests []Descriptor `json:"manifests"` - - // Annotations contains arbitrary metadata for the image index. - Annotations map[string]string `json:"annotations,omitempty"` -} diff --git a/modules/packages/container/oci/reference.go b/modules/packages/container/oci/reference.go deleted file mode 100644 index 7ec399255d..0000000000 --- a/modules/packages/container/oci/reference.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "regexp" -) - -var referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) - -type Reference string - -func (r Reference) Validate() bool { - return referencePattern.MatchString(string(r)) -} diff --git a/modules/proxy/proxy.go b/modules/proxy/proxy.go index f0cd366c12..1a6bdad7fb 100644 --- a/modules/proxy/proxy.go +++ b/modules/proxy/proxy.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "strings" "sync" "code.gitea.io/gitea/modules/log" @@ -82,3 +83,16 @@ func Proxy() func(req *http.Request) (*url.URL, error) { return http.ProxyFromEnvironment(req) } } + +// EnvWithProxy returns os.Environ(), with a https_proxy env, if the given url +// needs to be proxied. +func EnvWithProxy(u *url.URL) []string { + envs := os.Environ() + if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { + if Match(u.Host) { + envs = append(envs, "https_proxy="+GetProxyURL()) + } + } + + return envs +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 7bcda0fe45..b9a72ad573 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -211,6 +211,7 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m IsEmpty: !opts.AutoInit, TrustModel: opts.TrustModel, IsMirror: opts.IsMirror, + DefaultBranch: opts.DefaultBranch, } var rollbackRepo *repo_model.Repository diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index 293071bdce..e620422bcb 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") // Check Owner team. - ownerTeam, err := org.GetOwnerTeam() + ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) assert.NoError(t, err, "GetOwnerTeam") assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") @@ -63,7 +63,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { } } // Get fresh copy of Owner team after creating repos. - ownerTeam, err = org.GetOwnerTeam() + ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) assert.NoError(t, err, "GetOwnerTeam") // Create teams and check repositories. diff --git a/modules/repository/repo.go b/modules/repository/repo.go index d1a70e7c15..c03e469990 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repoPath := repo_model.RepoPath(u.Name, opts.RepoName) if u.IsOrganization() { - t, err := organization.OrgFromUser(u).GetOwnerTeam() + t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) if err != nil { return nil, err } diff --git a/modules/setting/packages.go b/modules/setting/packages.go index d0cd80aa03..84da4eb53e 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -25,6 +25,8 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 + LimitSizeCargo int64 + LimitSizeChef int64 LimitSizeComposer int64 LimitSizeConan int64 LimitSizeConda int64 @@ -65,6 +67,8 @@ func newPackages() { } Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") + Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") + Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN") Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA") diff --git a/modules/setting/service.go b/modules/setting/service.go index 7b4bfc5c7b..1d33ac6bce 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -46,6 +46,8 @@ var Service = struct { RecaptchaSecret string RecaptchaSitekey string RecaptchaURL string + CfTurnstileSecret string + CfTurnstileSitekey string HcaptchaSecret string HcaptchaSitekey string McaptchaSecret string @@ -137,6 +139,8 @@ func newService() { Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("") Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("") Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/") + Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("") + Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("") Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("") Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("") Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 23cd90553e..a68a46f7ad 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -61,6 +61,7 @@ const ( ReCaptcha = "recaptcha" HCaptcha = "hcaptcha" MCaptcha = "mcaptcha" + CfTurnstile = "cfturnstile" ) // settings diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 16f3d9dd26..ee4bec4df7 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -63,6 +63,7 @@ type Repository struct { Language string `json:"language"` LanguagesURL string `json:"languages_url"` HTMLURL string `json:"html_url"` + Link string `json:"link"` SSHURL string `json:"ssh_url"` CloneURL string `json:"clone_url"` OriginalURL string `json:"original_url"` diff --git a/modules/structs/org_type.go b/modules/structs/visible_type.go similarity index 87% rename from modules/structs/org_type.go rename to modules/structs/visible_type.go index 69f323fa99..b5ff353b87 100644 --- a/modules/structs/org_type.go +++ b/modules/structs/visible_type.go @@ -3,7 +3,7 @@ package structs -// VisibleType defines the visibility (Organization only) +// VisibleType defines the visibility of user and org type VisibleType int const ( @@ -13,11 +13,11 @@ const ( // VisibleTypeLimited Visible for every connected user VisibleTypeLimited - // VisibleTypePrivate Visible only for organization's members + // VisibleTypePrivate Visible only for self or admin user VisibleTypePrivate ) -// VisibilityModes is a map of org Visibility types +// VisibilityModes is a map of Visibility types var VisibilityModes = map[string]VisibleType{ "public": VisibleTypePublic, "limited": VisibleTypeLimited, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a390d94592..7afc3aa59b 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -72,6 +72,10 @@ func NewFuncMap() []template.FuncMap { return setting.StaticURLPrefix + "/assets" }, "AppUrl": func() string { + // The usage of AppUrl should be avoided as much as possible, + // because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect. + // And it's difficult for Gitea to guess absolute URL correctly with zero configuration, + // because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea. return setting.AppURL }, "AppVer": func() string { diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go new file mode 100644 index 0000000000..38d0233446 --- /dev/null +++ b/modules/turnstile/turnstile.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package turnstile + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" +) + +// Response is the structure of JSON returned from API +type Response struct { + Success bool `json:"success"` + ChallengeTS string `json:"challenge_ts"` + Hostname string `json:"hostname"` + ErrorCodes []ErrorCode `json:"error-codes"` + Action string `json:"login"` + Cdata string `json:"cdata"` +} + +// Verify calls Cloudflare Turnstile API to verify token +func Verify(ctx context.Context, response string) (bool, error) { + // Cloudflare turnstile official access instruction address: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ + post := url.Values{ + "secret": {setting.Service.CfTurnstileSecret}, + "response": {response}, + } + // Basically a copy of http.PostForm, but with a context + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + "https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode())) + if err != nil { + return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err) + } + + var jsonResponse Response + if err := json.Unmarshal(body, &jsonResponse); err != nil { + return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err) + } + + var respErr error + if len(jsonResponse.ErrorCodes) > 0 { + respErr = jsonResponse.ErrorCodes[0] + } + return jsonResponse.Success, respErr +} + +// ErrorCode is a reCaptcha error +type ErrorCode string + +// String fulfills the Stringer interface +func (e ErrorCode) String() string { + switch e { + case "missing-input-secret": + return "The secret parameter was not passed." + case "invalid-input-secret": + return "The secret parameter was invalid or did not exist." + case "missing-input-response": + return "The response parameter was not passed." + case "invalid-input-response": + return "The response parameter is invalid or has expired." + case "bad-request": + return "The request was rejected because it was malformed." + case "timeout-or-duplicate": + return "The response parameter has already been validated before." + case "internal-error": + return "An internal error happened while validating the response. The request can be retried." + } + return string(e) +} + +// Error fulfills the error interface +func (e ErrorCode) Error() string { + return e.String() +} diff --git a/services/activitypub/keypair.go b/modules/util/keypair.go similarity index 76% rename from services/activitypub/keypair.go rename to modules/util/keypair.go index 299bdc43e3..5a3ce715a4 100644 --- a/services/activitypub/keypair.go +++ b/modules/util/keypair.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package activitypub +package util import ( "crypto/rand" @@ -10,11 +10,9 @@ import ( "encoding/pem" ) -const rsaBits = 2048 - -// GenerateKeyPair generates a public and private keypair for signing actions by users for activitypub purposes -func GenerateKeyPair() (string, string, error) { - priv, _ := rsa.GenerateKey(rand.Reader, rsaBits) +// GenerateKeyPair generates a public and private keypair +func GenerateKeyPair(bits int) (string, string, error) { + priv, _ := rsa.GenerateKey(rand.Reader, bits) privPem, err := pemBlockForPriv(priv) if err != nil { return "", "", err diff --git a/services/activitypub/keypair_test.go b/modules/util/keypair_test.go similarity index 93% rename from services/activitypub/keypair_test.go rename to modules/util/keypair_test.go index 888254c9da..c6f68c845a 100644 --- a/services/activitypub/keypair_test.go +++ b/modules/util/keypair_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package activitypub +package util import ( "crypto" @@ -17,7 +17,7 @@ import ( ) func TestKeygen(t *testing.T) { - priv, pub, err := GenerateKeyPair() + priv, pub, err := GenerateKeyPair(2048) assert.NoError(t, err) assert.NotEmpty(t, priv) @@ -28,7 +28,7 @@ func TestKeygen(t *testing.T) { } func TestSignUsingKeys(t *testing.T) { - priv, pub, err := GenerateKeyPair() + priv, pub, err := GenerateKeyPair(2048) assert.NoError(t, err) privPem, _ := pem.Decode([]byte(priv)) diff --git a/modules/validation/binding.go b/modules/validation/binding.go index ef0d01e80f..1f904979ff 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/git" "gitea.com/go-chi/binding" @@ -17,15 +18,14 @@ import ( const ( // ErrGitRefName is git reference name error ErrGitRefName = "GitRefNameError" - // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" - // ErrRegexPattern is returned when a regex pattern is invalid ErrRegexPattern = "RegexPattern" - // ErrUsername is username error ErrUsername = "UsernameError" + // ErrInvalidGroupTeamMap is returned when a group team mapping is invalid + ErrInvalidGroupTeamMap = "InvalidGroupTeamMap" ) // AddBindingRules adds additional binding rules @@ -37,6 +37,7 @@ func AddBindingRules() { addRegexPatternRule() addGlobOrRegexPatternRule() addUsernamePatternRule() + addValidGroupTeamMapRule() } func addGitRefNameBindingRule() { @@ -167,6 +168,23 @@ func addUsernamePatternRule() { }) } +func addValidGroupTeamMapRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "ValidGroupTeamMap") + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + _, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val)) + if err != nil { + errs.Add([]string{name}, ErrInvalidGroupTeamMap, err.Error()) + return false, errs + } + + return true, errs + }, + }) +} + func portOnly(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 733f00a1d5..8b74a864d9 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -136,6 +136,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) case validation.ErrUsername: data["ErrorMsg"] = trName + l.Tr("form.username_error") + case validation.ErrInvalidGroupTeamMap: + data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) default: msg := errs[0].Classification if msg != "" && errs[0].Message != "" { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f384056613..f784b10c8d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -477,6 +477,7 @@ include_error = ` must contain substring '%s'.` glob_pattern_error = ` glob pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` +invalid_group_team_map_error = ` mapping is invalid: %s` unknown_error = Unknown error: captcha_incorrect = The CAPTCHA code is incorrect. password_not_match = The passwords do not match. @@ -2758,6 +2759,8 @@ auths.oauth2_required_claim_value_helper = Set this value to restrict login from auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional) auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) +auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above) +auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group. auths.enable_auto_register = Enable Auto Registration auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time @@ -3145,6 +3148,8 @@ keywords = Keywords details = Details details.author = Author details.project_site = Project Site +details.repository_site = Repository Site +details.documentation_site = Documentation Site details.license = License assets = Assets versions = Versions @@ -3152,6 +3157,14 @@ versions.on = on versions.view_all = View all dependency.id = ID dependency.version = Version +cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): +cargo.install = To install the package using Cargo, run the following command: +cargo.documentation = For more information on the Cargo registry, see the documentation. +cargo.details.repository_site = Repository Site +cargo.details.documentation_site = Documentation Site +chef.registry = Setup this registry in your ~/.chef/config.rb file: +chef.install = To install the package, run the following command: +chef.documentation = For more information on the Chef registry, see the documentation. composer.registry = Setup this registry in your ~/.composer/config.json file: composer.install = To install the package using Composer, run the following command: composer.documentation = For more information on the Composer registry, see the documentation. @@ -3168,8 +3181,6 @@ 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 -container.details.documentation_site = Documentation Site container.pull = Pull the image from the command line: container.digest = Digest: container.documentation = For more information on the Container registry, see the documentation. @@ -3203,8 +3214,6 @@ npm.dependencies.optional = Optional Dependencies npm.details.tag = Tag pub.install = To install the package using Dart, run the following command: pub.documentation = For more information on the Pub registry, see the documentation. -pub.details.repository_site = Repository Site -pub.details.documentation_site = Documentation Site pypi.requires = Requires Python pypi.install = To install the package using pip, run the following command: pypi.documentation = For more information on the PyPI registry, see the documentation. @@ -3228,6 +3237,15 @@ settings.delete.description = Deleting a package is permanent and cannot be undo settings.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure? settings.delete.success = The package has been deleted. settings.delete.error = Failed to delete the package. +owner.settings.cargo.title = Cargo Registry Index +owner.settings.cargo.initialize = Initialize Index +owner.settings.cargo.initialize.description = To use the Cargo registry a special index git repository is needed. Here you can (re)create it with the required config. +owner.settings.cargo.initialize.error = Failed to initialize Cargo index: %v +owner.settings.cargo.initialize.success = The Cargo index was successfully created. +owner.settings.cargo.rebuild = Rebuild Index +owner.settings.cargo.rebuild.description = If the index is out of sync with the cargo packages stored you can rebuild it here. +owner.settings.cargo.rebuild.error = Failed to rebuild Cargo index: %v +owner.settings.cargo.rebuild.success = The Cargo index was successfully rebuild. owner.settings.cleanuprules.title = Manage Cleanup Rules owner.settings.cleanuprules.add = Add Cleanup Rule owner.settings.cleanuprules.edit = Edit Cleanup Rule @@ -3248,6 +3266,9 @@ owner.settings.cleanuprules.remove.days = Remove versions older than owner.settings.cleanuprules.remove.pattern = Remove versions matching owner.settings.cleanuprules.success.update = Cleanup rule has been updated. owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted. +owner.settings.chef.title = Chef Registry +owner.settings.chef.keypair = Generate key pair +owner.settings.chef.keypair.description = Generate a key pair used to authenticate against the Chef registry. The previous key can not be used afterwards. [secrets] secrets = Secrets diff --git a/public/img/svg/gitea-cargo.svg b/public/img/svg/gitea-cargo.svg new file mode 100644 index 0000000000..91d53941ca --- /dev/null +++ b/public/img/svg/gitea-cargo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/gitea-chef.svg b/public/img/svg/gitea-chef.svg new file mode 100644 index 0000000000..8f1cd6165c --- /dev/null +++ b/public/img/svg/gitea-chef.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 7a07fea815..9f77367d6f 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -14,6 +14,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/packages/cargo" + "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/composer" "code.gitea.io/gitea/routers/api/packages/conan" "code.gitea.io/gitea/routers/api/packages/conda" @@ -53,6 +55,7 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { &auth.Basic{}, &nuget.Auth{}, &conan.Auth{}, + &chef.Auth{}, } if setting.Service.EnableReverseProxyAuth { authMethods = append(authMethods, &auth.ReverseProxy{}) @@ -71,6 +74,39 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { }) r.Group("/{username}", func() { + r.Group("/cargo", func() { + r.Group("/api/v1/crates", func() { + r.Get("", cargo.SearchPackages) + r.Put("/new", reqPackageAccess(perm.AccessModeWrite), cargo.UploadPackage) + r.Group("/{package}", func() { + r.Group("/{version}", func() { + r.Get("/download", cargo.DownloadPackageFile) + r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage) + r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), cargo.UnyankPackage) + }) + r.Get("/owners", cargo.ListOwners) + }) + }) + }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/chef", func() { + r.Group("/api/v1", func() { + r.Get("/universe", chef.PackagesUniverse) + r.Get("/search", chef.EnumeratePackages) + r.Group("/cookbooks", func() { + r.Get("", chef.EnumeratePackages) + r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage) + r.Group("/{name}", func() { + r.Get("", chef.PackageMetadata) + r.Group("/versions/{version}", func() { + r.Get("", chef.PackageVersionMetadata) + r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackageVersion) + r.Get("/download", chef.DownloadPackage) + }) + r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackage) + }) + }) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/composer", func() { r.Get("/packages.json", composer.ServiceIndex) r.Get("/search.json", composer.SearchPackages) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go new file mode 100644 index 0000000000..e0bf5da13a --- /dev/null +++ b/routers/api/packages/cargo/cargo.go @@ -0,0 +1,281 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + cargo_module "code.gitea.io/gitea/modules/packages/cargo" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/services/convert" + packages_service "code.gitea.io/gitea/services/packages" + cargo_service "code.gitea.io/gitea/services/packages/cargo" +) + +// https://doc.rust-lang.org/cargo/reference/registries.html#web-api +type StatusResponse struct { + OK bool `json:"ok"` + Errors []StatusMessage `json:"errors,omitempty"` +} + +type StatusMessage struct { + Message string `json:"detail"` +} + +func apiError(ctx *context.Context, status int, obj interface{}) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.JSON(status, StatusResponse{ + OK: false, + Errors: []StatusMessage{ + { + Message: message, + }, + }, + }) + }) +} + +type SearchResult struct { + Crates []*SearchResultCrate `json:"crates"` + Meta SearchResultMeta `json:"meta"` +} + +type SearchResultCrate struct { + Name string `json:"name"` + LatestVersion string `json:"max_version"` + Description string `json:"description"` +} + +type SearchResultMeta struct { + Total int64 `json:"total"` +} + +// https://doc.rust-lang.org/cargo/reference/registries.html#search +func SearchPackages(ctx *context.Context) { + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + perPage := ctx.FormInt("per_page") + paginator := db.ListOptions{ + Page: page, + PageSize: convert.ToCorrectPageSize(perPage), + } + + pvs, total, err := packages_model.SearchLatestVersions( + ctx, + &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeCargo, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + IsInternal: util.OptionalBoolFalse, + Paginator: &paginator, + }, + ) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + crates := make([]*SearchResultCrate, 0, len(pvs)) + for _, pd := range pds { + crates = append(crates, &SearchResultCrate{ + Name: pd.Package.Name, + LatestVersion: pd.Version.Version, + Description: pd.Metadata.(*cargo_module.Metadata).Description, + }) + } + + ctx.JSON(http.StatusOK, SearchResult{ + Crates: crates, + Meta: SearchResultMeta{ + Total: total, + }, + }) +} + +type Owners struct { + Users []OwnerUser `json:"users"` +} + +type OwnerUser struct { + ID int64 `json:"id"` + Login string `json:"login"` + Name string `json:"name"` +} + +// https://doc.rust-lang.org/cargo/reference/registries.html#owners-list +func ListOwners(ctx *context.Context) { + ctx.JSON(http.StatusOK, Owners{ + Users: []OwnerUser{ + { + ID: ctx.Package.Owner.ID, + Login: ctx.Package.Owner.Name, + Name: ctx.Package.Owner.DisplayName(), + }, + }, + }) +} + +// DownloadPackageFile serves the content of a package +func DownloadPackageFile(ctx *context.Context) { + s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeCargo, + Name: ctx.Params("package"), + Version: ctx.Params("version"), + }, + &packages_service.PackageFileInfo{ + Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", ctx.Params("package"), ctx.Params("version"))), + }, + ) + if err != nil { + if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer s.Close() + + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) +} + +// https://doc.rust-lang.org/cargo/reference/registries.html#publish +func UploadPackage(ctx *context.Context) { + defer ctx.Req.Body.Close() + + cp, err := cargo_module.ParsePackage(ctx.Req.Body) + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + + buf, err := packages_module.CreateHashedBufferFromReader(cp.Content, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + if buf.Size() != cp.ContentSize { + apiError(ctx, http.StatusBadRequest, "invalid content size") + return + } + + pv, _, err := packages_service.CreatePackageAndAddFile( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeCargo, + Name: cp.Name, + Version: cp.Version, + }, + SemverCompatible: true, + Creator: ctx.Doer, + Metadata: cp.Metadata, + VersionProperties: map[string]string{ + cargo_module.PropertyYanked: strconv.FormatBool(false), + }, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", cp.Name, cp.Version)), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion: + 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 + } + + if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil { + if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + log.Error("Rollback creation of package version: %v", err) + } + + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.JSON(http.StatusOK, StatusResponse{OK: true}) +} + +// https://doc.rust-lang.org/cargo/reference/registries.html#yank +func YankPackage(ctx *context.Context) { + yankPackage(ctx, true) +} + +// https://doc.rust-lang.org/cargo/reference/registries.html#unyank +func UnyankPackage(ctx *context.Context) { + yankPackage(ctx, false) +} + +func yankPackage(ctx *context.Context, yank bool) { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"), ctx.Params("version")) + if err != nil { + if err == packages_model.ErrPackageNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, cargo_module.PropertyYanked) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pps) == 0 { + apiError(ctx, http.StatusInternalServerError, "Property not found") + return + } + + pp := pps[0] + pp.Value = strconv.FormatBool(yank) + + if err := packages_model.UpdateProperty(ctx, pp); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.JSON(http.StatusOK, StatusResponse{OK: true}) +} diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go new file mode 100644 index 0000000000..69f7b763ab --- /dev/null +++ b/routers/api/packages/chef/auth.go @@ -0,0 +1,270 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "crypto" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "hash" + "math/big" + "net/http" + "path" + "regexp" + "strconv" + "strings" + "time" + + user_model "code.gitea.io/gitea/models/user" + chef_module "code.gitea.io/gitea/modules/packages/chef" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/auth" +) + +const ( + maxTimeDifference = 10 * time.Minute +) + +var ( + algorithmPattern = regexp.MustCompile(`algorithm=(\w+)`) + versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`) + authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`) +) + +// Documentation: +// https://docs.chef.io/server/api_chef_server/#required-headers +// https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md +// https://github.com/chef/mixlib-authentication/blob/bc8adbef833d4be23dc78cb23e6fe44b51ebc34f/lib/mixlib/authentication/signedheaderauth.rb + +type Auth struct{} + +func (a *Auth) Name() string { + return "chef" +} + +// Verify extracts the user from the signed request +// If the request is signed with the user private key the user is verified. +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { + u, err := getUserFromRequest(req) + if err != nil { + return nil, err + } + if u == nil { + return nil, nil + } + + pub, err := getUserPublicKey(u) + if err != nil { + return nil, err + } + + if err := verifyTimestamp(req); err != nil { + return nil, err + } + + version, err := getSignVersion(req) + if err != nil { + return nil, err + } + + if err := verifySignedHeaders(req, version, pub.(*rsa.PublicKey)); err != nil { + return nil, err + } + + return u, nil +} + +func getUserFromRequest(req *http.Request) (*user_model.User, error) { + username := req.Header.Get("X-Ops-Userid") + if username == "" { + return nil, nil + } + + return user_model.GetUserByName(req.Context(), username) +} + +func getUserPublicKey(u *user_model.User) (crypto.PublicKey, error) { + pubKey, err := user_model.GetSetting(u.ID, chef_module.SettingPublicPem) + if err != nil { + return nil, err + } + + pubPem, _ := pem.Decode([]byte(pubKey)) + + return x509.ParsePKIXPublicKey(pubPem.Bytes) +} + +func verifyTimestamp(req *http.Request) error { + hdr := req.Header.Get("X-Ops-Timestamp") + if hdr == "" { + return util.NewInvalidArgumentErrorf("X-Ops-Timestamp header missing") + } + + ts, err := time.Parse(time.RFC3339, hdr) + if err != nil { + return err + } + + diff := time.Now().UTC().Sub(ts) + if diff < 0 { + diff = -diff + } + + if diff > maxTimeDifference { + return fmt.Errorf("time difference") + } + + return nil +} + +func getSignVersion(req *http.Request) (string, error) { + hdr := req.Header.Get("X-Ops-Sign") + if hdr == "" { + return "", util.NewInvalidArgumentErrorf("X-Ops-Sign header missing") + } + + m := versionPattern.FindStringSubmatch(hdr) + if len(m) != 2 { + return "", util.NewInvalidArgumentErrorf("invalid X-Ops-Sign header") + } + + switch m[1] { + case "1.0", "1.1", "1.2", "1.3": + default: + return "", util.NewInvalidArgumentErrorf("unsupported version") + } + + version := m[1] + + m = algorithmPattern.FindStringSubmatch(hdr) + if len(m) == 2 && m[1] != "sha1" && !(m[1] == "sha256" && version == "1.3") { + return "", util.NewInvalidArgumentErrorf("unsupported algorithm") + } + + return version, nil +} + +func verifySignedHeaders(req *http.Request, version string, pub *rsa.PublicKey) error { + authorizationData, err := getAuthorizationData(req) + if err != nil { + return err + } + + checkData := buildCheckData(req, version) + + switch version { + case "1.3": + return verifyDataNew(authorizationData, checkData, pub, crypto.SHA256) + case "1.2": + return verifyDataNew(authorizationData, checkData, pub, crypto.SHA1) + default: + return verifyDataOld(authorizationData, checkData, pub) + } +} + +func getAuthorizationData(req *http.Request) ([]byte, error) { + valueList := make(map[int]string) + for k, vs := range req.Header { + if m := authorizationPattern.FindStringSubmatch(k); m != nil { + index, _ := strconv.Atoi(m[1]) + var v string + if len(vs) == 0 { + v = "" + } else { + v = vs[0] + } + valueList[index] = v + } + } + + tmp := make([]string, len(valueList)) + for k, v := range valueList { + if k > len(tmp) { + return nil, fmt.Errorf("invalid X-Ops-Authorization headers") + } + tmp[k-1] = v + } + + return base64.StdEncoding.DecodeString(strings.Join(tmp, "")) +} + +func buildCheckData(req *http.Request, version string) []byte { + username := req.Header.Get("X-Ops-Userid") + if version != "1.0" && version != "1.3" { + sum := sha1.Sum([]byte(username)) + username = base64.StdEncoding.EncodeToString(sum[:]) + } + + var data string + if version == "1.3" { + data = fmt.Sprintf( + "Method:%s\nPath:%s\nX-Ops-Content-Hash:%s\nX-Ops-Sign:version=%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s\nX-Ops-Server-API-Version:%s", + req.Method, + path.Clean(req.URL.Path), + req.Header.Get("X-Ops-Content-Hash"), + version, + req.Header.Get("X-Ops-Timestamp"), + username, + req.Header.Get("X-Ops-Server-Api-Version"), + ) + } else { + sum := sha1.Sum([]byte(path.Clean(req.URL.Path))) + data = fmt.Sprintf( + "Method:%s\nHashed Path:%s\nX-Ops-Content-Hash:%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s", + req.Method, + base64.StdEncoding.EncodeToString(sum[:]), + req.Header.Get("X-Ops-Content-Hash"), + req.Header.Get("X-Ops-Timestamp"), + username, + ) + } + + return []byte(data) +} + +func verifyDataNew(signature, data []byte, pub *rsa.PublicKey, algo crypto.Hash) error { + var h hash.Hash + if algo == crypto.SHA256 { + h = sha256.New() + } else { + h = sha1.New() + } + if _, err := h.Write(data); err != nil { + return err + } + + return rsa.VerifyPKCS1v15(pub, algo, h.Sum(nil), signature) +} + +func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error { + c := new(big.Int) + m := new(big.Int) + m.SetBytes(signature) + e := big.NewInt(int64(pub.E)) + c.Exp(m, e, pub.N) + + out := c.Bytes() + + skip := 0 + for i := 2; i < len(out); i++ { + if i+1 >= len(out) { + break + } + if out[i] == 0xFF && out[i+1] == 0 { + skip = i + 2 + break + } + } + + if !util.SliceEqual(out[skip:], data) { + return fmt.Errorf("could not verify signature") + } + + return nil +} diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go new file mode 100644 index 0000000000..28d07dea47 --- /dev/null +++ b/routers/api/packages/chef/chef.go @@ -0,0 +1,404 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + chef_module "code.gitea.io/gitea/modules/packages/chef" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + type Error struct { + ErrorMessages []string `json:"error_messages"` + } + + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.JSON(status, Error{ + ErrorMessages: []string{message}, + }) + }) +} + +func PackagesUniverse(ctx *context.Context) { + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeChef, + IsInternal: util.OptionalBoolFalse, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + type VersionInfo struct { + LocationType string `json:"location_type"` + LocationPath string `json:"location_path"` + DownloadURL string `json:"download_url"` + Dependencies map[string]string `json:"dependencies"` + } + + baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1" + + universe := make(map[string]map[string]*VersionInfo) + for _, pd := range pds { + if _, ok := universe[pd.Package.Name]; !ok { + universe[pd.Package.Name] = make(map[string]*VersionInfo) + } + universe[pd.Package.Name][pd.Version.Version] = &VersionInfo{ + LocationType: "opscode", + LocationPath: baseURL, + DownloadURL: fmt.Sprintf("%s/cookbooks/%s/versions/%s/download", baseURL, url.PathEscape(pd.Package.Name), pd.Version.Version), + Dependencies: pd.Metadata.(*chef_module.Metadata).Dependencies, + } + } + + ctx.JSON(http.StatusOK, universe) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_list.rb +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_search.rb +func EnumeratePackages(ctx *context.Context) { + opts := &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeChef, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + IsInternal: util.OptionalBoolFalse, + Paginator: db.NewAbsoluteListOptions( + ctx.FormInt("start"), + ctx.FormInt("items"), + ), + } + + switch strings.ToLower(ctx.FormTrim("order")) { + case "recently_updated", "recently_added": + opts.Sort = packages_model.SortCreatedDesc + default: + opts.Sort = packages_model.SortNameAsc + } + + pvs, total, err := packages_model.SearchLatestVersions(ctx, opts) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + type Item struct { + CookbookName string `json:"cookbook_name"` + CookbookMaintainer string `json:"cookbook_maintainer"` + CookbookDescription string `json:"cookbook_description"` + Cookbook string `json:"cookbook"` + } + + type Result struct { + Start int `json:"start"` + Total int `json:"total"` + Items []*Item `json:"items"` + } + + baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1/cookbooks/" + + items := make([]*Item, 0, len(pds)) + for _, pd := range pds { + metadata := pd.Metadata.(*chef_module.Metadata) + + items = append(items, &Item{ + CookbookName: pd.Package.Name, + CookbookMaintainer: metadata.Author, + CookbookDescription: metadata.Description, + Cookbook: baseURL + url.PathEscape(pd.Package.Name), + }) + } + + skip, _ := opts.Paginator.GetSkipTake() + + ctx.JSON(http.StatusOK, &Result{ + Start: skip, + Total: int(total), + Items: items, + }) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb +func PackageMetadata(ctx *context.Context) { + packageName := ctx.Params("name") + + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + sort.Slice(pds, func(i, j int) bool { + return pds[i].SemVer.LessThan(pds[j].SemVer) + }) + + type Result struct { + Name string `json:"name"` + Maintainer string `json:"maintainer"` + Description string `json:"description"` + Category string `json:"category"` + LatestVersion string `json:"latest_version"` + SourceURL string `json:"source_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Deprecated bool `json:"deprecated"` + Versions []string `json:"versions"` + } + + baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s/versions/", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(packageName)) + + versions := make([]string, 0, len(pds)) + for _, pd := range pds { + versions = append(versions, baseURL+pd.Version.Version) + } + + latest := pds[len(pds)-1] + + metadata := latest.Metadata.(*chef_module.Metadata) + + ctx.JSON(http.StatusOK, &Result{ + Name: latest.Package.Name, + Maintainer: metadata.Author, + Description: metadata.Description, + LatestVersion: baseURL + latest.Version.Version, + SourceURL: metadata.RepositoryURL, + CreatedAt: latest.Version.CreatedUnix.AsLocalTime(), + UpdatedAt: latest.Version.CreatedUnix.AsLocalTime(), + Deprecated: false, + Versions: versions, + }) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb +func PackageVersionMetadata(ctx *context.Context) { + packageName := ctx.Params("name") + packageVersion := strings.ReplaceAll(ctx.Params("version"), "_", ".") // Chef calls this endpoint with "_" instead of "."?! + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion) + if err != nil { + if err == packages_model.ErrPackageNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + type Result struct { + Version string `json:"version"` + TarballFileSize int64 `json:"tarball_file_size"` + PublishedAt time.Time `json:"published_at"` + Cookbook string `json:"cookbook"` + File string `json:"file"` + License string `json:"license"` + Dependencies map[string]string `json:"dependencies"` + } + + baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(pd.Package.Name)) + + metadata := pd.Metadata.(*chef_module.Metadata) + + ctx.JSON(http.StatusOK, &Result{ + Version: pd.Version.Version, + TarballFileSize: pd.Files[0].Blob.Size, + PublishedAt: pd.Version.CreatedUnix.AsLocalTime(), + Cookbook: baseURL, + File: fmt.Sprintf("%s/versions/%s/download", baseURL, pd.Version.Version), + License: metadata.License, + Dependencies: metadata.Dependencies, + }) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_share.rb +func UploadPackage(ctx *context.Context) { + file, _, err := ctx.Req.FormFile("tarball") + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + defer file.Close() + + buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := chef_module.ParsePackage(buf) + 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 + } + + _, _, err = packages_service.CreatePackageAndAddFile( + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeChef, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + SemverCompatible: true, + Metadata: pck.Metadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: strings.ToLower(pck.Version + ".tar.gz"), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion: + apiError(ctx, http.StatusBadRequest, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.JSON(http.StatusCreated, make(map[any]any)) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_download.rb +func DownloadPackage(ctx *context.Context) { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"), ctx.Params("version")) + if err != nil { + if err == packages_model.ErrPackageNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pf := pd.Files[0].File + + 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(), + }) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb +func DeletePackageVersion(ctx *context.Context) { + packageName := ctx.Params("name") + packageVersion := ctx.Params("version") + + err := packages_service.RemovePackageVersionByNameAndVersion( + ctx.Doer, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeChef, + Name: packageName, + Version: packageVersion, + }, + ) + if err != nil { + if err == packages_model.ErrPackageNotExist { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusOK) +} + +// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb +func DeletePackage(ctx *context.Context) { + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name")) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if len(pvs) == 0 { + apiError(ctx, http.StatusNotFound, err) + return + } + + for _, pv := range pvs { + if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ctx.Status(http.StatusOK) +} diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index c22cfb5009..883fe73cbd 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -22,19 +22,23 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" + + digest "github.com/opencontainers/go-digest" ) // maximum size of a container manifest // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests const maxManifestSize = 10 * 1024 * 1024 -var imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) +var ( + imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) + referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) +) type containerHeaders struct { Status int @@ -434,16 +438,16 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - digest := ctx.Params("digest") + d := ctx.Params("digest") - if !oci.Digest(digest).Validate() { + if digest.Digest(d).Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.Params("image"), - Digest: digest, + Digest: d, }) } @@ -498,14 +502,14 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - digest := ctx.Params("digest") + d := ctx.Params("digest") - if !oci.Digest(digest).Validate() { + if digest.Digest(d).Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } - if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), digest); err != nil { + if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), d); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } @@ -520,15 +524,15 @@ func UploadManifest(ctx *context.Context) { reference := ctx.Params("reference") mci := &manifestCreationInfo{ - MediaType: oci.MediaType(ctx.Req.Header.Get("Content-Type")), + MediaType: ctx.Req.Header.Get("Content-Type"), Owner: ctx.Package.Owner, Creator: ctx.Doer, Image: ctx.Params("image"), Reference: reference, - IsTagged: !oci.Digest(reference).Validate(), + IsTagged: digest.Digest(reference).Validate() != nil, } - if mci.IsTagged && !oci.Reference(reference).Validate() { + if mci.IsTagged && !referencePattern.MatchString(reference) { apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid")) return } @@ -571,7 +575,7 @@ func UploadManifest(ctx *context.Context) { }) } -func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { +func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { reference := ctx.Params("reference") opts := &container_model.BlobSearchOptions{ @@ -579,14 +583,24 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe Image: ctx.Params("image"), IsManifest: true, } - if oci.Digest(reference).Validate() { + + if digest.Digest(reference).Validate() == nil { opts.Digest = reference - } else if oci.Reference(reference).Validate() { + } else if referencePattern.MatchString(reference) { opts.Tag = reference } else { return nil, container_model.ErrContainerBlobNotExist } + return opts, nil +} + +func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { + opts, err := getBlobSearchOptionsFromContext(ctx) + if err != nil { + return nil, err + } + return workaroundGetContainerBlob(ctx, opts) } @@ -643,18 +657,8 @@ func GetManifest(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests func DeleteManifest(ctx *context.Context) { - reference := ctx.Params("reference") - - opts := &container_model.BlobSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Image: ctx.Params("image"), - IsManifest: true, - } - if oci.Digest(reference).Validate() { - opts.Digest = reference - } else if oci.Reference(reference).Validate() { - opts.Tag = reference - } else { + opts, err := getBlobSearchOptionsFromContext(ctx) + if err != nil { apiErrorDefined(ctx, errManifestUnknown) return } diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 491fb70639..6167d00f31 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -19,14 +19,28 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + digest "github.com/opencontainers/go-digest" + oci "github.com/opencontainers/image-spec/specs-go/v1" ) +func isValidMediaType(mt string) bool { + return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") +} + +func isImageManifestMediaType(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") +} + +func isImageIndexMediaType(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") +} + // manifestCreationInfo describes a manifest to create type manifestCreationInfo struct { - MediaType oci.MediaType + MediaType string Owner *user_model.User Creator *user_model.User Image string @@ -36,12 +50,12 @@ type manifestCreationInfo struct { } func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - var schema oci.SchemaMediaBase - if err := json.NewDecoder(buf).Decode(&schema); err != nil { + var index oci.Index + if err := json.NewDecoder(buf).Decode(&index); err != nil { return "", err } - if schema.SchemaVersion != 2 { + if index.SchemaVersion != 2 { return "", errUnsupported.WithMessage("Schema version is not supported") } @@ -49,17 +63,17 @@ func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffe return "", err } - if !mci.MediaType.IsValid() { - mci.MediaType = schema.MediaType - if !mci.MediaType.IsValid() { + if !isValidMediaType(mci.MediaType) { + mci.MediaType = index.MediaType + if !isValidMediaType(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if mci.MediaType.IsImageManifest() { + if isImageManifestMediaType(mci.MediaType) { d, err := processImageManifest(mci, buf) return d, err - } else if mci.MediaType.IsImageIndex() { + } else if isImageIndexMediaType(mci.MediaType) { d, err := processImageManifestIndex(mci, buf) return d, err } @@ -204,7 +218,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H } for _, manifest := range index.Manifests { - if !manifest.MediaType.IsImageManifest() { + if !isImageManifestMediaType(manifest.MediaType) { return errManifestInvalid } @@ -348,8 +362,8 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } type blobReference struct { - Digest oci.Digest - MediaType oci.MediaType + Digest digest.Digest + MediaType string Name string File *packages_model.PackageFileDescriptor ExpectedSize int64 @@ -383,7 +397,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } props := map[string]string{ - container_module.PropertyMediaType: string(ref.MediaType), + container_module.PropertyMediaType: ref.MediaType, container_module.PropertyDigest: string(ref.Digest), } for name, value := range props { @@ -428,7 +442,7 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack manifestDigest := digestFromHashSummer(buf) err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ - Digest: oci.Digest(manifestDigest), + Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, Name: container_model.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 638fdbec6d..861c171758 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -507,7 +507,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { var err error if assignOrg { - ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org")) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.Params(":org")) if err != nil { if organization.IsErrOrgNotExist(err) { redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org")) @@ -687,7 +687,7 @@ func Routes(ctx gocontext.Context) *web.Route { } // Get user from session if logged in. - m.Use(context.APIAuth(group)) + m.Use(auth.APIAuth(group)) m.Use(context.ToggleAPI(&context.ToggleOptions{ SignInRequired: setting.Service.RequireSignInView, diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 5ffefc4862..ab077090d1 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, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] + // enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant] // - name: q // in: query // description: name filter diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index e4c7eb7041..5b564a8066 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -108,7 +108,7 @@ func CreateFork(ctx *context.APIContext) { if form.Organization == nil { forker = ctx.Doer } else { - org, err := organization.GetOrgByName(*form.Organization) + org, err := organization.GetOrgByName(ctx, *form.Organization) if err != nil { if organization.IsErrOrgNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index e0cae5f82c..1426d1dbcc 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -468,7 +468,7 @@ func CreateOrgRepo(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateRepoOption) - org, err := organization.GetOrgByName(ctx.Params(":org")) + org, err := organization.GetOrgByName(ctx, ctx.Params(":org")) if err != nil { if organization.IsErrOrgNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 1bc166902c..8ce45720fe 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -204,6 +204,8 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { GroupClaimName: form.Oauth2GroupClaimName, RestrictedGroup: form.Oauth2RestrictedGroup, AdminGroup: form.Oauth2AdminGroup, + GroupTeamMap: form.Oauth2GroupTeamMap, + GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval, } } diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 6c409c6b9d..47a0daa06d 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" + "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" @@ -267,5 +268,11 @@ func LinkAccountPostRegister(ctx *context.Context) { return } + source := authSource.Cfg.(*oauth2.Source) + if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + handleSignIn(ctx, u, false) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index be60a0c73b..a11417da16 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -17,7 +17,9 @@ import ( "code.gitea.io/gitea/models/auth" org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -27,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" auth_service "code.gitea.io/gitea/services/auth" + source_service "code.gitea.io/gitea/services/auth/source" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" @@ -963,12 +966,19 @@ func SignInOAuthCallback(ctx *context.Context) { IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm), } - setUserGroupClaims(authSource, u, &gothUser) + source := authSource.Cfg.(*oauth2.Source) + + setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser) if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { // error already handled return } + + if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } } else { // no existing user is found, request attach or new account showLinkingLogin(ctx, gothUser) @@ -979,7 +989,7 @@ func SignInOAuthCallback(ctx *context.Context) { handleOAuth2SignIn(ctx, authSource, u, gothUser) } -func claimValueToStringSlice(claimValue interface{}) []string { +func claimValueToStringSet(claimValue interface{}) container.Set[string] { var groups []string switch rawGroup := claimValue.(type) { @@ -993,37 +1003,45 @@ func claimValueToStringSlice(claimValue interface{}) []string { str := fmt.Sprintf("%s", rawGroup) groups = strings.Split(str, ",") } - return groups + return container.SetOf(groups...) } -func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool { - source := loginSource.Cfg.(*oauth2.Source) - if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") { - return false +func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error { + if source.GroupTeamMap != "" || source.GroupTeamMapRemoval { + groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap) + if err != nil { + return err + } + + groups := getClaimedGroups(source, gothUser) + + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil { + return err + } } + return nil +} + +func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] { groupClaims, has := gothUser.RawData[source.GroupClaimName] if !has { - return false + return nil } - groups := claimValueToStringSlice(groupClaims) + return claimValueToStringSet(groupClaims) +} + +func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool { + groups := getClaimedGroups(source, gothUser) wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted if source.AdminGroup != "" { - u.IsAdmin = false + u.IsAdmin = groups.Contains(source.AdminGroup) } if source.RestrictedGroup != "" { - u.IsRestricted = false - } - - for _, g := range groups { - if source.AdminGroup != "" && g == source.AdminGroup { - u.IsAdmin = true - } else if source.RestrictedGroup != "" && g == source.RestrictedGroup { - u.IsRestricted = true - } + u.IsRestricted = groups.Contains(source.RestrictedGroup) } return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted @@ -1070,6 +1088,15 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model needs2FA = err == nil } + oauth2Source := source.Cfg.(*oauth2.Source) + groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap) + if err != nil { + ctx.ServerError("UnmarshalGroupTeamMapping", err) + return + } + + groups := getClaimedGroups(oauth2Source, &gothUser) + // If this user is enrolled in 2FA and this source doesn't override it, // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. if !needs2FA { @@ -1088,7 +1115,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model u.SetLastLogin() // Update GroupClaims - changed := setUserGroupClaims(source, u, &gothUser) + changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser) cols := []string{"last_login_unix"} if changed { cols = append(cols, "is_admin", "is_restricted") @@ -1099,6 +1126,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model return } + if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + } + // update external user information if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { if !errors.Is(err, util.ErrNotExist) { @@ -1121,7 +1155,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model return } - changed := setUserGroupClaims(source, u, &gothUser) + changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser) if changed { if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil { ctx.ServerError("UpdateUserCols", err) @@ -1129,6 +1163,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model } } + if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + } + if err := updateSession(ctx, nil, map[string]interface{}{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, @@ -1188,15 +1229,9 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res } if oauth2Source.RequiredClaimValue != "" { - groups := claimValueToStringSlice(claimInterface) - found := false - for _, group := range groups { - if group == oauth2Source.RequiredClaimValue { - found = true - break - } - } - if !found { + groups := claimValueToStringSet(claimInterface) + + if !groups.Contains(oauth2Source.RequiredClaimValue) { return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID} } } diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 7c375a085f..76dc769c65 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -73,7 +73,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio var content, desc, title string - link := &feeds.Link{Href: act.GetCommentLink()} + link := &feeds.Link{Href: act.GetCommentHTMLURL()} // title title = act.ActUser.DisplayName() + " " diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 80135ca2d0..21d25bd90a 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -84,3 +84,23 @@ func PackagesRulePreview(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview) } + +func InitializeCargoIndex(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("packages.title") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsSettingsPackages"] = true + + shared.InitializeCargoIndex(ctx, ctx.ContextUser) + + ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name)) +} + +func RebuildCargoIndex(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("packages.title") + ctx.Data["PageIsOrgSettings"] = true + ctx.Data["PageIsSettingsPackages"] = true + + shared.RebuildCargoIndex(ctx, ctx.ContextUser) + + ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name)) +} diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index 89c86e764e..e82b94b9e8 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -424,60 +424,40 @@ func (h *serviceHandler) sendFile(contentType, file string) { // one or more key=value pairs separated by colons var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`) -func getGitConfig(ctx gocontext.Context, option, dir string) string { - out, _, err := git.NewCommand(ctx, "config").AddDynamicArguments(option).RunStdString(&git.RunOpts{Dir: dir}) - if err != nil { - log.Error("%v - %s", err, out) +func prepareGitCmdWithAllowedService(service string, h *serviceHandler) (*git.Command, error) { + if service == "receive-pack" && h.cfg.ReceivePack { + return git.NewCommand(h.r.Context(), "receive-pack"), nil } - return out[0 : len(out)-1] + if service == "upload-pack" && h.cfg.UploadPack { + return git.NewCommand(h.r.Context(), "upload-pack"), nil + } + + return nil, fmt.Errorf("service %q is not allowed", service) } -func getConfigSetting(ctx gocontext.Context, service, dir string) bool { - service = strings.ReplaceAll(service, "-", "") - setting := getGitConfig(ctx, "http."+service, dir) - - if service == "uploadpack" { - return setting != "false" - } - - return setting == "true" -} - -func hasAccess(ctx gocontext.Context, service string, h serviceHandler, checkContentType bool) bool { - if checkContentType { - if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) { - return false - } - } - - if !(service == "upload-pack" || service == "receive-pack") { - return false - } - if service == "receive-pack" { - return h.cfg.ReceivePack - } - if service == "upload-pack" { - return h.cfg.UploadPack - } - - return getConfigSetting(ctx, service, h.dir) -} - -func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) { +func serviceRPC(h *serviceHandler, service string) { defer func() { if err := h.r.Body.Close(); err != nil { log.Error("serviceRPC: Close: %v", err) } }() - if !hasAccess(ctx, service, h, true) { + expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) + if h.r.Header.Get("Content-Type") != expectedContentType { + log.Error("Content-Type (%q) doesn't match expected: %q", h.r.Header.Get("Content-Type"), expectedContentType) + h.w.WriteHeader(http.StatusUnauthorized) + return + } + + cmd, err := prepareGitCmdWithAllowedService(service, h) + if err != nil { + log.Error("Failed to prepareGitCmdWithService: %v", err) h.w.WriteHeader(http.StatusUnauthorized) return } h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) - var err error reqBody := h.r.Body // Handle GZIP. @@ -498,8 +478,7 @@ func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) { } var stderr bytes.Buffer - // the service is generated by ourselves, so it's safe to trust it - cmd := git.NewCommand(h.r.Context(), git.ToTrustedCmdArgs([]string{service})...).AddArguments("--stateless-rpc").AddDynamicArguments(h.dir) + cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.dir) cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir)) if err := cmd.Run(&git.RunOpts{ Dir: h.dir, @@ -520,7 +499,7 @@ func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) { func ServiceUploadPack(ctx *context.Context) { h := httpBase(ctx) if h != nil { - serviceRPC(ctx, *h, "upload-pack") + serviceRPC(h, "upload-pack") } } @@ -528,7 +507,7 @@ func ServiceUploadPack(ctx *context.Context) { func ServiceReceivePack(ctx *context.Context) { h := httpBase(ctx) if h != nil { - serviceRPC(ctx, *h, "receive-pack") + serviceRPC(h, "receive-pack") } } @@ -537,7 +516,7 @@ func getServiceType(r *http.Request) string { if !strings.HasPrefix(serviceType, "git-") { return "" } - return strings.Replace(serviceType, "git-", "", 1) + return strings.TrimPrefix(serviceType, "git-") } func updateServerInfo(ctx gocontext.Context, dir string) []byte { @@ -563,16 +542,15 @@ func GetInfoRefs(ctx *context.Context) { return } h.setHeaderNoCache() - if hasAccess(ctx, getServiceType(h.r), *h, false) { - service := getServiceType(h.r) - + service := getServiceType(h.r) + cmd, err := prepareGitCmdWithAllowedService(service, h) + if err == nil { if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { h.environ = append(h.environ, "GIT_PROTOCOL="+protocol) } h.environ = append(os.Environ(), h.environ...) - // the service is generated by ourselves, so we can trust it - refs, _, err := git.NewCommand(ctx, git.ToTrustedCmdArgs([]string{service})...).AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir}) + refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir}) if err != nil { log.Error(fmt.Sprintf("%v - %s", err, string(refs))) } diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 01421dc927..66e8920bd9 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) { } ctx.Data["OrgLabels"] = orgLabels - org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName) + org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName) if err != nil { ctx.ServerError("GetOrgByName", err) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 11d336d4ec..ad17005d90 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -339,8 +339,8 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch } ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL() - ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL() + ctx.Data["HeadBranchLink"] = pull.GetHeadBranchLink() + ctx.Data["BaseBranchLink"] = pull.GetBaseBranchLink() } // PrepareMergedViewPullInfo show meta information for a merged pull request view page diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 609bd6c853..1b1887d65a 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -569,6 +569,7 @@ func SearchRepo(ctx *context.Context) { Mirror: repo.IsMirror, Stars: repo.NumStars, HTMLURL: repo.HTMLURL(), + Link: repo.Link(), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, } } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 2cc263e5bb..5c30795f22 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -1006,7 +1006,7 @@ func AddTeamPost(ctx *context.Context) { return } - team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name) + team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name) if err != nil { if organization.IsErrTeamNotExist(err) { ctx.Flash.Error(ctx.Tr("form.team_not_exist")) diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go index b9aa40bdd2..30c25374d1 100644 --- a/routers/web/shared/packages/packages.go +++ b/routers/web/shared/packages/packages.go @@ -13,9 +13,11 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" + cargo_service "code.gitea.io/gitea/services/packages/cargo" container_service "code.gitea.io/gitea/services/packages/container" ) @@ -223,3 +225,23 @@ func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *pack return nil } + +func InitializeCargoIndex(ctx *context.Context, owner *user_model.User) { + err := cargo_service.InitializeIndexRepository(ctx, owner, owner) + if err != nil { + log.Error("InitializeIndexRepository failed: %v", err) + ctx.Flash.Error(ctx.Tr("packages.owner.settings.cargo.initialize.error", err)) + } else { + ctx.Flash.Success(ctx.Tr("packages.owner.settings.cargo.initialize.success")) + } +} + +func RebuildCargoIndex(ctx *context.Context, owner *user_model.User) { + err := cargo_service.RebuildIndex(ctx, owner, owner) + if err != nil { + log.Error("RebuildIndex failed: %v", err) + ctx.Flash.Error(ctx.Tr("packages.owner.settings.cargo.rebuild.error", err)) + } else { + ctx.Flash.Success(ctx.Tr("packages.owner.settings.cargo.rebuild.success")) + } +} diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index f6f7195adf..0d2eb14c20 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -5,10 +5,14 @@ package setting import ( "net/http" + "strings" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + chef_module "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" shared "code.gitea.io/gitea/routers/web/shared/packages" ) @@ -77,3 +81,39 @@ func PackagesRulePreview(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview) } + +func InitializeCargoIndex(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("packages.title") + ctx.Data["PageIsSettingsPackages"] = true + + shared.InitializeCargoIndex(ctx, ctx.Doer) + + ctx.Redirect(setting.AppSubURL + "/user/settings/packages") +} + +func RebuildCargoIndex(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("packages.title") + ctx.Data["PageIsSettingsPackages"] = true + + shared.RebuildCargoIndex(ctx, ctx.Doer) + + ctx.Redirect(setting.AppSubURL + "/user/settings/packages") +} + +func RegenerateChefKeyPair(ctx *context.Context) { + priv, pub, err := util.GenerateKeyPair(chef_module.KeyBits) + if err != nil { + ctx.ServerError("GenerateKeyPair", err) + return + } + + if err := user_model.SetUserSetting(ctx.Doer.ID, chef_module.SettingPublicPem, pub); err != nil { + ctx.ServerError("SetUserSetting", err) + return + } + + ctx.ServeContent(strings.NewReader(priv), &context.ServeHeaderOptions{ + ContentType: "application/x-pem-file", + Filename: ctx.Doer.Name + ".priv", + }) +} diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index e01f3cdeea..f0f053a514 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -137,11 +137,8 @@ func ProfilePost(ctx *context.Context) { return } - // Update the language to the one we just set - middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0) - log.Trace("User settings updated: %s", ctx.Doer.Name) - ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_profile_success")) + ctx.Flash.Success(ctx.Tr("settings.update_profile_success")) ctx.Redirect(setting.AppSubURL + "/user/settings") } diff --git a/routers/web/web.go b/routers/web/web.go index b7128fc3a9..88e27ad678 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -203,7 +203,7 @@ func Routes(ctx gocontext.Context) *web.Route { } // Get user from session if logged in. - common = append(common, context.Auth(group)) + common = append(common, auth_service.Auth(group)) // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route common = append(common, middleware.GetHead) @@ -468,6 +468,11 @@ func RegisterRoutes(m *web.Route) { m.Get("/preview", user_setting.PackagesRulePreview) }) }) + m.Group("/cargo", func() { + m.Post("/initialize", user_setting.InitializeCargoIndex) + m.Post("/rebuild", user_setting.RebuildCargoIndex) + }) + m.Post("/chef/regenerate_keypair", user_setting.RegenerateChefKeyPair) }, packagesEnabled) m.Group("/secrets", func() { m.Get("", user_setting.Secrets) @@ -818,6 +823,10 @@ func RegisterRoutes(m *web.Route) { m.Get("/preview", org.PackagesRulePreview) }) }) + m.Group("/cargo", func() { + m.Post("/initialize", org.InitializeCargoIndex) + m.Post("/rebuild", org.RebuildCargoIndex) + }) }, packagesEnabled) }, func(ctx *context.Context) { ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable diff --git a/services/activitypub/user_settings.go b/services/activitypub/user_settings.go index ec5fa59842..2d156c17e6 100644 --- a/services/activitypub/user_settings.go +++ b/services/activitypub/user_settings.go @@ -5,8 +5,11 @@ package activitypub import ( user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" ) +const rsaBits = 2048 + // GetKeyPair function returns a user's private and public keys func GetKeyPair(user *user_model.User) (pub, priv string, err error) { var settings map[string]*user_model.Setting @@ -14,7 +17,7 @@ func GetKeyPair(user *user_model.User) (pub, priv string, err error) { if err != nil { return } else if len(settings) == 0 { - if priv, pub, err = GenerateKeyPair(); err != nil { + if priv, pub, err = util.GenerateKeyPair(rsaBits); err != nil { return } if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, priv); err != nil { diff --git a/services/auth/middleware.go b/services/auth/middleware.go new file mode 100644 index 0000000000..cccaab2998 --- /dev/null +++ b/services/auth/middleware.go @@ -0,0 +1,60 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/web/middleware" +) + +// Auth is a middleware to authenticate a web user +func Auth(authMethod Method) func(*context.Context) { + return func(ctx *context.Context) { + if err := authShared(ctx, authMethod); err != nil { + log.Error("Failed to verify user: %v", err) + ctx.Error(http.StatusUnauthorized, "Verify") + return + } + if ctx.Doer == nil { + // ensure the session uid is deleted + _ = ctx.Session.Delete("uid") + } + } +} + +// APIAuth is a middleware to authenticate an api user +func APIAuth(authMethod Method) func(*context.APIContext) { + return func(ctx *context.APIContext) { + if err := authShared(ctx.Context, authMethod); err != nil { + ctx.Error(http.StatusUnauthorized, "APIAuth", err) + } + } +} + +func authShared(ctx *context.Context, authMethod Method) error { + var err error + ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if err != nil { + return err + } + if ctx.Doer != nil { + if ctx.Locale.Language() != ctx.Doer.Language { + ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) + } + ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName + ctx.IsSigned = true + ctx.Data["IsSigned"] = ctx.IsSigned + ctx.Data["SignedUser"] = ctx.Doer + ctx.Data["SignedUserID"] = ctx.Doer.ID + ctx.Data["SignedUserName"] = ctx.Doer.Name + ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin + } else { + ctx.Data["SignedUserID"] = int64(0) + ctx.Data["SignedUserName"] = "" + } + return nil +} diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 321cf5540d..fba8da7934 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -10,9 +10,10 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/util" + source_service "code.gitea.io/gitea/services/auth/source" "code.gitea.io/gitea/services/mailer" user_service "code.gitea.io/gitea/services/user" ) @@ -64,61 +65,66 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str } if user != nil { - if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { - orgCache := make(map[string]*organization.Organization) - teamCache := make(map[string]*organization.Team) - source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache) - } if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) { - return user, asymkey_model.RewriteAllPublicKeys() + if err := asymkey_model.RewriteAllPublicKeys(); err != nil { + return user, err + } + } + } else { + // Fallback. + if len(sr.Username) == 0 { + sr.Username = userName + } + + if len(sr.Mail) == 0 { + sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) + } + + user = &user_model.User{ + LowerName: strings.ToLower(sr.Username), + Name: sr.Username, + FullName: composeFullName(sr.Name, sr.Surname, sr.Username), + Email: sr.Mail, + LoginType: source.authSource.Type, + LoginSource: source.authSource.ID, + LoginName: userName, + IsAdmin: sr.IsAdmin, + } + overwriteDefault := &user_model.CreateUserOverwriteOptions{ + IsRestricted: util.OptionalBoolOf(sr.IsRestricted), + IsActive: util.OptionalBoolTrue, + } + + err := user_model.CreateUser(user, overwriteDefault) + if err != nil { + return user, err + } + + mailer.SendRegisterNotifyMail(user) + + if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) { + if err := asymkey_model.RewriteAllPublicKeys(); err != nil { + return user, err + } + } + if len(source.AttributeAvatar) > 0 { + if err := user_service.UploadAvatar(user, sr.Avatar); err != nil { + return user, err + } } - return user, nil } - // Fallback. - if len(sr.Username) == 0 { - sr.Username = userName - } - - if len(sr.Mail) == 0 { - sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) - } - - user = &user_model.User{ - LowerName: strings.ToLower(sr.Username), - Name: sr.Username, - FullName: composeFullName(sr.Name, sr.Surname, sr.Username), - Email: sr.Mail, - LoginType: source.authSource.Type, - LoginSource: source.authSource.ID, - LoginName: userName, - IsAdmin: sr.IsAdmin, - } - overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsRestricted: util.OptionalBoolOf(sr.IsRestricted), - IsActive: util.OptionalBoolTrue, - } - - err := user_model.CreateUser(user, overwriteDefault) - if err != nil { - return user, err - } - - mailer.SendRegisterNotifyMail(user) - - if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) { - err = asymkey_model.RewriteAllPublicKeys() - } - if err == nil && len(source.AttributeAvatar) > 0 { - _ = user_service.UploadAvatar(user, sr.Avatar) - } if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { - orgCache := make(map[string]*organization.Organization) - teamCache := make(map[string]*organization.Team) - source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache) + groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap) + if err != nil { + return user, err + } + if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil { + return user, err + } } - return user, err + return user, nil } // IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go deleted file mode 100644 index 95a6084922..0000000000 --- a/services/auth/source/ldap/source_group_sync.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package ldap - -import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" -) - -// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships -func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) { - var err error - if source.GroupsEnabled && source.GroupTeamMapRemoval { - // when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships - removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache) - } - for orgName, teamNames := range ldapTeamAdd { - org, ok := orgCache[orgName] - if !ok { - org, err = organization.GetOrgByName(orgName) - if err != nil { - // organization must be created before LDAP group sync - log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err) - continue - } - orgCache[orgName] = org - } - - for _, teamName := range teamNames { - team, ok := teamCache[orgName+teamName] - if !ok { - team, err = org.GetTeam(teamName) - if err != nil { - // team must be created before LDAP group sync - log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err) - continue - } - teamCache[orgName+teamName] = team - } - if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); !isMember && err == nil { - log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name) - } else { - continue - } - err := models.AddTeamMember(team, user.ID) - if err != nil { - log.Error("LDAP group sync: Could not add user to team: %v", err) - } - } - } -} - -// remove membership to organizations/teams if user is not member of corresponding LDAP group -// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y" -// then users membership gets removed for all organizations/teams mapped by LDAP group "y" -func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) { - var err error - for orgName, teamNames := range ldapTeamRemove { - org, ok := orgCache[orgName] - if !ok { - org, err = organization.GetOrgByName(orgName) - if err != nil { - // organization must be created before LDAP group sync - log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err) - continue - } - orgCache[orgName] = org - } - for _, teamName := range teamNames { - team, ok := teamCache[orgName+teamName] - if !ok { - team, err = org.GetTeam(teamName) - if err != nil { - // team must must be created before LDAP group sync - log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err) - continue - } - } - if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); isMember && err == nil { - log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name) - } else { - continue - } - err = models.RemoveTeamMember(team, user.ID) - if err != nil { - log.Error("LDAP group sync: Could not remove user from team: %v", err) - } - } - } -} diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index 16f13029f9..5a2d25b0c4 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -11,26 +11,24 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" "github.com/go-ldap/ldap/v3" ) // SearchResult : user data type SearchResult struct { - Username string // Username - Name string // Name - Surname string // Surname - Mail string // E-mail address - SSHPublicKey []string // SSH Public Key - IsAdmin bool // if user is administrator - IsRestricted bool // if user is restricted - LowerName string // LowerName - Avatar []byte - LdapTeamAdd map[string][]string // organizations teams to add - LdapTeamRemove map[string][]string // organizations teams to remove + Username string // Username + Name string // Name + Surname string // Surname + Mail string // E-mail address + SSHPublicKey []string // SSH Public Key + IsAdmin bool // if user is administrator + IsRestricted bool // if user is restricted + LowerName string // LowerName + Avatar []byte + Groups container.Set[string] } func (source *Source) sanitizedUserQuery(username string) (string, bool) { @@ -196,9 +194,8 @@ 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, applyGroupFilter bool) []string { - var ldapGroups []string - var searchFilter string +func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] { + ldapGroups := make(container.Set[string]) groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter) if !ok { @@ -210,12 +207,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr return ldapGroups } + var searchFilter string 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( groupDN, ldap.ScopeWholeSubtree, @@ -237,44 +234,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr log.Error("LDAP search was successful, but found no DN!") continue } - ldapGroups = append(ldapGroups, entry.DN) + ldapGroups.Add(entry.DN) } return ldapGroups } -// parse LDAP groups and return map of ldap groups to organizations teams -func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string { - ldapGroupsToTeams := make(map[string]map[string][]string) - err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams) - if err != nil { - log.Error("Failed to unmarshall LDAP teams map: %v", err) - return ldapGroupsToTeams - } - return ldapGroupsToTeams -} - -// getMappedMemberships : returns the organizations and teams to modify the users membership -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{} - membershipsToRemove := map[string][]string{} - for group, memberships := range ldapGroupsToTeams { - isUserInGroup := util.SliceContainsString(usersLdapGroups, group) - if isUserInGroup { - for org, teams := range memberships { - membershipsToAdd[org] = teams - } - } else if !isUserInGroup { - for org, teams := range memberships { - membershipsToRemove[org] = teams - } - } - } - return membershipsToAdd, membershipsToRemove -} - func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { if strings.ToLower(source.UserUID) == "dn" { return entry.DN @@ -399,23 +364,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname) mail := sr.Entries[0].GetAttributeValue(source.AttributeMail) - teamsToAdd := make(map[string][]string) - teamsToRemove := make(map[string][]string) - - // Check group membership - if source.GroupsEnabled { - userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0]) - usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true) - - if source.GroupFilter != "" && len(usersLdapGroups) == 0 { - return nil - } - - if source.GroupTeamMap != "" || source.GroupTeamMapRemoval { - teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup) - } - } - if isAttributeSSHPublicKeySet { sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey) } @@ -431,6 +379,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar) } + // Check group membership + var usersLdapGroups container.Set[string] + if source.GroupsEnabled { + userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0]) + usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true) + + if source.GroupFilter != "" && len(usersLdapGroups) == 0 { + return nil + } + } + if !directBind && source.AttributesInBind { // binds user (checking password) after looking-up attributes in BindDN context err = bindUser(l, userDN, passwd) @@ -440,17 +399,16 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR } return &SearchResult{ - LowerName: strings.ToLower(username), - Username: username, - Name: firstname, - Surname: surname, - Mail: mail, - SSHPublicKey: sshPublicKey, - IsAdmin: isAdmin, - IsRestricted: isRestricted, - Avatar: Avatar, - LdapTeamAdd: teamsToAdd, - LdapTeamRemove: teamsToRemove, + LowerName: strings.ToLower(username), + Username: username, + Name: firstname, + Surname: surname, + Mail: mail, + SSHPublicKey: sshPublicKey, + IsAdmin: isAdmin, + IsRestricted: isRestricted, + Avatar: Avatar, + Groups: usersLdapGroups, } } @@ -512,33 +470,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { result := make([]*SearchResult, 0, len(sr.Entries)) for _, v := range sr.Entries { - teamsToAdd := make(map[string][]string) - teamsToRemove := make(map[string][]string) - + var usersLdapGroups container.Set[string] if source.GroupsEnabled { userAttributeListedInGroup := source.getUserAttributeListedInGroup(v) if source.GroupFilter != "" { - usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true) + 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) + usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false) } } user := &SearchResult{ - Username: v.GetAttributeValue(source.AttributeUsername), - Name: v.GetAttributeValue(source.AttributeName), - Surname: v.GetAttributeValue(source.AttributeSurname), - Mail: v.GetAttributeValue(source.AttributeMail), - IsAdmin: checkAdmin(l, source, v.DN), - LdapTeamAdd: teamsToAdd, - LdapTeamRemove: teamsToRemove, + Username: v.GetAttributeValue(source.AttributeUsername), + Name: v.GetAttributeValue(source.AttributeName), + Surname: v.GetAttributeValue(source.AttributeSurname), + Mail: v.GetAttributeValue(source.AttributeMail), + IsAdmin: checkAdmin(l, source, v.DN), + Groups: usersLdapGroups, } if !user.IsAdmin { diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 73e8309aca..4571ff6540 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -13,8 +13,10 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + source_service "code.gitea.io/gitea/services/auth/source" user_service "code.gitea.io/gitea/services/user" ) @@ -65,6 +67,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { orgCache := make(map[string]*organization.Organization) teamCache := make(map[string]*organization.Team) + groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap) + if err != nil { + return err + } + for _, su := range sr { select { case <-ctx.Done(): @@ -173,7 +180,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } // Synchronize LDAP groups with organization and team memberships if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) { - source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache) + if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil { + log.Error("SyncGroupsToTeamsCached: %v", err) + } } } diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 0abebc04ec..675005e55a 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -8,13 +8,6 @@ import ( "code.gitea.io/gitea/modules/json" ) -// ________ _____ __ .__ ________ -// \_____ \ / _ \ __ ___/ |_| |__ \_____ \ -// / | \ / /_\ \| | \ __\ | \ / ____/ -// / | \/ | \ | /| | | Y \/ \ -// \_______ /\____|__ /____/ |__| |___| /\_______ \ -// \/ \/ \/ \/ - // Source holds configuration for the OAuth2 login source. type Source struct { Provider string @@ -24,13 +17,15 @@ type Source struct { CustomURLMapping *CustomURLMapping IconURL string - Scopes []string - RequiredClaimName string - RequiredClaimValue string - GroupClaimName string - AdminGroup string - RestrictedGroup string - SkipLocalTwoFA bool `json:",omitempty"` + Scopes []string + RequiredClaimName string + RequiredClaimValue string + GroupClaimName string + AdminGroup string + GroupTeamMap string + GroupTeamMapRemoval bool + RestrictedGroup string + SkipLocalTwoFA bool `json:",omitempty"` // reference to the authSource authSource *auth.Source diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go new file mode 100644 index 0000000000..20b6095345 --- /dev/null +++ b/services/auth/source/source_group_sync.go @@ -0,0 +1,116 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package source + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" +) + +type syncType int + +const ( + syncAdd syncType = iota + syncRemove +) + +// SyncGroupsToTeams maps authentication source groups to organization and team memberships +func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool) error { + orgCache := make(map[string]*organization.Organization) + teamCache := make(map[string]*organization.Team) + return SyncGroupsToTeamsCached(ctx, user, sourceUserGroups, sourceGroupTeamMapping, performRemoval, orgCache, teamCache) +} + +// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships +func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error { + membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping) + + if performRemoval { + if err := syncGroupsToTeamsCached(ctx, user, membershipsToRemove, syncRemove, orgCache, teamCache); err != nil { + return fmt.Errorf("could not sync[remove] user groups: %w", err) + } + } + + if err := syncGroupsToTeamsCached(ctx, user, membershipsToAdd, syncAdd, orgCache, teamCache); err != nil { + return fmt.Errorf("could not sync[add] user groups: %w", err) + } + + return nil +} + +func resolveMappedMemberships(sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string) (map[string][]string, map[string][]string) { + membershipsToAdd := map[string][]string{} + membershipsToRemove := map[string][]string{} + for group, memberships := range sourceGroupTeamMapping { + isUserInGroup := sourceUserGroups.Contains(group) + if isUserInGroup { + for org, teams := range memberships { + membershipsToAdd[org] = teams + } + } else { + for org, teams := range memberships { + membershipsToRemove[org] = teams + } + } + } + return membershipsToAdd, membershipsToRemove +} + +func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeamMap map[string][]string, action syncType, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error { + for orgName, teamNames := range orgTeamMap { + var err error + org, ok := orgCache[orgName] + if !ok { + org, err = organization.GetOrgByName(ctx, orgName) + if err != nil { + if organization.IsErrOrgNotExist(err) { + // organization must be created before group sync + log.Warn("group sync: Could not find organisation %s: %v", orgName, err) + continue + } + return err + } + orgCache[orgName] = org + } + for _, teamName := range teamNames { + team, ok := teamCache[orgName+teamName] + if !ok { + team, err = org.GetTeam(ctx, teamName) + if err != nil { + if organization.IsErrTeamNotExist(err) { + // team must be created before group sync + log.Warn("group sync: Could not find team %s: %v", teamName, err) + continue + } + return err + } + teamCache[orgName+teamName] = team + } + + isMember, err := organization.IsTeamMember(ctx, org.ID, team.ID, user.ID) + if err != nil { + return err + } + + if action == syncAdd && !isMember { + if err := models.AddTeamMember(team, user.ID); err != nil { + log.Error("group sync: Could not add user to team: %v", err) + return err + } + } else if action == syncRemove && isMember { + if err := models.RemoveTeamMember(team, user.ID); err != nil { + log.Error("group sync: Could not remove user from team: %v", err) + return err + } + } + } + } + return nil +} diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index aad0e39591..2e6560ec0c 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -16,7 +16,7 @@ import ( "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" - packages_service "code.gitea.io/gitea/services/packages" + packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup" repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" ) @@ -152,7 +152,7 @@ func registerCleanupPackages() { OlderThan: 24 * time.Hour, }, func(ctx context.Context, _ *user_model.User, config Config) error { realConfig := config.(*OlderThanConfig) - return packages_service.Cleanup(ctx, realConfig.OlderThan) + return packages_cleanup_service.Cleanup(ctx, realConfig.OlderThan) }) } diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index 0cede07f95..5625aa1e2e 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -72,13 +72,15 @@ type AuthenticationForm struct { Oauth2GroupClaimName string Oauth2AdminGroup string Oauth2RestrictedGroup string + Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"` + Oauth2GroupTeamMapRemoval bool SkipLocalTwoFA bool SSPIAutoCreateUsers bool SSPIAutoActivateUsers bool SSPIStripDomainNames bool SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"` SSPIDefaultLanguage string - GroupTeamMap string + GroupTeamMap string `binding:"ValidGroupTeamMap"` GroupTeamMapRemoval bool } diff --git a/services/forms/package_form.go b/services/forms/package_form.go index e78e64ef7e..b22ed47c77 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,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"` + Type string `binding:"Required;In(cargo,chef,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/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 7dee90352e..126d2bf354 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/proxy" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -215,6 +216,8 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo return nil, false } + envs := proxy.EnvWithProxy(remoteURL.URL) + stdoutBuilder := strings.Builder{} stderrBuilder := strings.Builder{} if err := cmd. @@ -222,6 +225,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo Run(&git.RunOpts{ Timeout: timeout, Dir: repoPath, + Env: envs, Stdout: &stdoutBuilder, Stderr: &stderrBuilder, }); err != nil { diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go new file mode 100644 index 0000000000..e58a472816 --- /dev/null +++ b/services/packages/cargo/index.go @@ -0,0 +1,290 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "path" + "strconv" + "time" + + packages_model "code.gitea.io/gitea/models/packages" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + cargo_module "code.gitea.io/gitea/modules/packages/cargo" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + files_service "code.gitea.io/gitea/services/repository/files" +) + +const ( + IndexRepositoryName = "_cargo-index" + ConfigFileName = "config.json" +) + +// https://doc.rust-lang.org/cargo/reference/registries.html#index-format + +func BuildPackagePath(name string) string { + switch len(name) { + case 0: + panic("Cargo package name can not be empty") + case 1: + return path.Join("1", name) + case 2: + return path.Join("2", name) + case 3: + return path.Join("3", string(name[0]), name) + default: + return path.Join(name[0:2], name[2:4], name) + } +} + +func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error { + repo, err := getOrCreateIndexRepository(ctx, doer, owner) + if err != nil { + return err + } + + if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil { + return fmt.Errorf("createOrUpdateConfigFile: %w", err) + } + + return nil +} + +func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error { + repo, err := getOrCreateIndexRepository(ctx, doer, owner) + if err != nil { + return err + } + + ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo) + if err != nil { + return fmt.Errorf("GetPackagesByType: %w", err) + } + + return alterRepositoryContent( + ctx, + doer, + repo, + "Rebuild Cargo Index", + func(t *files_service.TemporaryUploadRepository) error { + // Remove all existing content but the Cargo config + files, err := t.LsFiles() + if err != nil { + return err + } + for i, file := range files { + if file == ConfigFileName { + files[i] = files[len(files)-1] + files = files[:len(files)-1] + break + } + } + if err := t.RemoveFilesFromIndex(files...); err != nil { + return err + } + + // Add all packages + for _, p := range ps { + if err := addOrUpdatePackageIndex(ctx, t, p); err != nil { + return err + } + } + + return nil + }, + ) +} + +func AddOrUpdatePackageIndex(ctx context.Context, doer, owner *user_model.User, packageID int64) error { + repo, err := getOrCreateIndexRepository(ctx, doer, owner) + if err != nil { + return err + } + + p, err := packages_model.GetPackageByID(ctx, packageID) + if err != nil { + return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err) + } + + return alterRepositoryContent( + ctx, + doer, + repo, + "Update "+p.Name, + func(t *files_service.TemporaryUploadRepository) error { + return addOrUpdatePackageIndex(ctx, t, p) + }, + ) +} + +type IndexVersionEntry struct { + Name string `json:"name"` + Version string `json:"vers"` + Dependencies []*cargo_module.Dependency `json:"deps"` + FileChecksum string `json:"cksum"` + Features map[string][]string `json:"features"` + Yanked bool `json:"yanked"` + Links string `json:"links,omitempty"` +} + +func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error { + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + PackageID: p.ID, + Sort: packages_model.SortVersionAsc, + }) + if err != nil { + return fmt.Errorf("SearchVersions[%s]: %w", p.Name, err) + } + if len(pvs) == 0 { + return nil + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + return fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err) + } + + var b bytes.Buffer + for _, pd := range pds { + metadata := pd.Metadata.(*cargo_module.Metadata) + + dependencies := metadata.Dependencies + if dependencies == nil { + dependencies = make([]*cargo_module.Dependency, 0) + } + + features := metadata.Features + if features == nil { + features = make(map[string][]string) + } + + yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked)) + entry, err := json.Marshal(&IndexVersionEntry{ + Name: pd.Package.Name, + Version: pd.Version.Version, + Dependencies: dependencies, + FileChecksum: pd.Files[0].Blob.HashSHA256, + Features: features, + Yanked: yanked, + Links: metadata.Links, + }) + if err != nil { + return err + } + + b.Write(entry) + b.WriteString("\n") + } + + return writeObjectToIndex(t, BuildPackagePath(pds[0].Package.LowerName), &b) +} + +func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) { + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + repo, err = repo_module.CreateRepository(doer, owner, repo_module.CreateRepoOptions{ + Name: IndexRepositoryName, + }) + if err != nil { + return nil, fmt.Errorf("CreateRepository: %w", err) + } + } else { + return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err) + } + } + + return repo, nil +} + +type Config struct { + DownloadURL string `json:"dl"` + APIURL string `json:"api"` +} + +func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error { + return alterRepositoryContent( + ctx, + doer, + repo, + "Initialize Cargo Config", + func(t *files_service.TemporaryUploadRepository) error { + var b bytes.Buffer + err := json.NewEncoder(&b).Encode(Config{ + DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates", + APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo", + }) + if err != nil { + return err + } + + return writeObjectToIndex(t, ConfigFileName, &b) + }, + ) +} + +// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository +func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error { + t, err := files_service.NewTemporaryUploadRepository(ctx, repo) + if err != nil { + return err + } + defer t.Close() + + var lastCommitID string + if err := t.Clone(repo.DefaultBranch); err != nil { + if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { + return err + } + if err := t.Init(); err != nil { + return err + } + } else { + if err := t.SetDefaultIndex(); err != nil { + return err + } + + commit, err := t.GetBranchCommit(repo.DefaultBranch) + if err != nil { + return err + } + + lastCommitID = commit.ID.String() + } + + if err := fn(t); err != nil { + return err + } + + treeHash, err := t.WriteTree() + if err != nil { + return err + } + + now := time.Now() + commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now) + if err != nil { + return err + } + + return t.Push(doer, commitHash, repo.DefaultBranch) +} + +func writeObjectToIndex(t *files_service.TemporaryUploadRepository, path string, r io.Reader) error { + hash, err := t.HashObject(r) + if err != nil { + return err + } + + return t.AddObjectToIndex("100644", hash, path) +} diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go new file mode 100644 index 0000000000..2d62a028a4 --- /dev/null +++ b/services/packages/cleanup/cleanup.go @@ -0,0 +1,154 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" + cargo_service "code.gitea.io/gitea/services/packages/cargo" + container_service "code.gitea.io/gitea/services/packages/container" +) + +// Cleanup removes expired package data +func Cleanup(taskCtx context.Context, olderThan time.Duration) error { + ctx, committer, err := db.TxContext(taskCtx) + if err != nil { + return err + } + defer committer.Close() + + err = packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error { + select { + case <-taskCtx.Done(): + return db.ErrCancelledf("While processing package cleanup rules") + default: + } + + if err := pcr.CompiledPattern(); err != nil { + return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err) + } + + olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays) + + packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type) + if err != nil { + return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err) + } + + for _, p := range packages { + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + PackageID: p.ID, + IsInternal: util.OptionalBoolFalse, + Sort: packages_model.SortCreatedDesc, + Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), + }) + if err != nil { + return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err) + } + versionDeleted := false + for _, pv := range pvs { + if pcr.Type == packages_model.TypeContainer { + if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { + return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err) + } else if skip { + log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version) + continue + } + } + + toMatch := pv.LowerVersion + if pcr.MatchFullName { + toMatch = p.LowerName + "/" + pv.LowerVersion + } + + if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { + log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version) + continue + } + if pv.CreatedUnix.AsLocalTime().After(olderThan) { + log.Debug("Rule[%d]: keep '%s/%s' (remove days)", pcr.ID, p.Name, pv.Version) + continue + } + if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) { + log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version) + continue + } + + log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version) + + if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return fmt.Errorf("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %w", pcr.ID, err) + } + + versionDeleted = true + } + + if versionDeleted { + if pcr.Type == packages_model.TypeCargo { + owner, err := user_model.GetUserByID(ctx, pcr.OwnerID) + if err != nil { + return fmt.Errorf("GetUserByID failed: %w", err) + } + if err := cargo_service.AddOrUpdatePackageIndex(ctx, owner, owner, p.ID); err != nil { + return fmt.Errorf("CleanupRule [%d]: cargo.AddOrUpdatePackageIndex failed: %w", pcr.ID, err) + } + } + } + } + return nil + }) + if err != nil { + return err + } + + if err := container_service.Cleanup(ctx, olderThan); err != nil { + return err + } + + ps, err := packages_model.FindUnreferencedPackages(ctx) + if err != nil { + return err + } + for _, p := range ps { + if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil { + return err + } + if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil { + return err + } + } + + pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan) + if err != nil { + return err + } + + for _, pb := range pbs { + if err := packages_model.DeleteBlobByID(ctx, pb.ID); err != nil { + return err + } + } + + if err := committer.Commit(); err != nil { + return err + } + + contentStore := packages_module.NewContentStore() + for _, pb := range pbs { + if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { + log.Error("Error deleting package blob [%v]: %v", pb.ID, err) + } + } + + return nil +} diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index d6d4d152c8..1a9ef26391 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -10,8 +10,9 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/util" + + digest "github.com/opencontainers/go-digest" ) // Cleanup removes expired container data @@ -87,7 +88,7 @@ func ShouldBeSkipped(ctx context.Context, pcr *packages_model.PackageCleanupRule } // Check if the version is a digest (or untagged) - if oci.Digest(pv.LowerVersion).Validate() { + if digest.Digest(pv.LowerVersion).Validate() == nil { // Check if there is another manifest referencing this version has, err := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{ PackageID: p.ID, diff --git a/services/packages/packages.go b/services/packages/packages.go index 9e52cb1450..3abca7337c 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "strings" - "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -22,7 +21,6 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - container_service "code.gitea.io/gitea/services/packages/container" ) var ( @@ -335,6 +333,10 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p var typeSpecificSize int64 switch packageType { + case packages_model.TypeCargo: + typeSpecificSize = setting.Packages.LimitSizeCargo + case packages_model.TypeChef: + typeSpecificSize = setting.Packages.LimitSizeChef case packages_model.TypeComposer: typeSpecificSize = setting.Packages.LimitSizeComposer case packages_model.TypeConan: @@ -448,123 +450,6 @@ func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) erro return packages_model.DeleteFileByID(ctx, pf.ID) } -// Cleanup removes expired package data -func Cleanup(taskCtx context.Context, olderThan time.Duration) error { - ctx, committer, err := db.TxContext(taskCtx) - if err != nil { - return err - } - defer committer.Close() - - err = packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error { - select { - case <-taskCtx.Done(): - return db.ErrCancelledf("While processing package cleanup rules") - default: - } - - if err := pcr.CompiledPattern(); err != nil { - return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err) - } - - olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays) - - packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type) - if err != nil { - return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err) - } - - for _, p := range packages { - pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - PackageID: p.ID, - IsInternal: util.OptionalBoolFalse, - Sort: packages_model.SortCreatedDesc, - Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), - }) - if err != nil { - return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err) - } - for _, pv := range pvs { - if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { - return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err) - } else if skip { - log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version) - continue - } - - toMatch := pv.LowerVersion - if pcr.MatchFullName { - toMatch = p.LowerName + "/" + pv.LowerVersion - } - - if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { - log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version) - continue - } - if pv.CreatedUnix.AsLocalTime().After(olderThan) { - log.Debug("Rule[%d]: keep '%s/%s' (remove days)", pcr.ID, p.Name, pv.Version) - continue - } - if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) { - log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version) - continue - } - - log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version) - - if err := DeletePackageVersionAndReferences(ctx, pv); err != nil { - return fmt.Errorf("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %w", pcr.ID, err) - } - } - } - return nil - }) - if err != nil { - return err - } - - if err := container_service.Cleanup(ctx, olderThan); err != nil { - return err - } - - ps, err := packages_model.FindUnreferencedPackages(ctx) - if err != nil { - return err - } - for _, p := range ps { - if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil { - return err - } - if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil { - return err - } - } - - pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan) - if err != nil { - return err - } - - for _, pb := range pbs { - if err := packages_model.DeleteBlobByID(ctx, pb.ID); err != nil { - return err - } - } - - if err := committer.Commit(); err != nil { - return err - } - - contentStore := packages_module.NewContentStore() - for _, pb := range pbs { - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob [%v]: %v", pb.ID, err) - } - } - - return nil -} - // GetFileStreamByPackageNameAndVersion returns the content of the specific package file func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo) (io.ReadSeekCloser, *packages_model.PackageFile, error) { log.Trace("Getting package file stream: %v, %v, %s, %s, %s, %s", pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version, pfi.Filename, pfi.CompositeKey) diff --git a/services/pull/merge.go b/services/pull/merge.go index edd5b601da..a3d69df8df 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -98,6 +98,9 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr } for _, ref := range refs { if ref.RefAction == references.XRefActionCloses { + if err := ref.LoadIssue(ctx); err != nil { + return "", "", err + } closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) } } diff --git a/services/pull/pull.go b/services/pull/pull.go index 317875d211..0d260c93b1 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -263,6 +263,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, return } + for _, pr := range prs { + log.Trace("Updating PR[%d]: composing new test task", pr.ID) + if pr.Flow == issues_model.PullRequestFlowGithub { + if err := PushToBaseRepo(ctx, pr); err != nil { + log.Error("PushToBaseRepo: %v", err) + continue + } + } else { + continue + } + + AddToTaskQueue(pr) + comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) + if err == nil && comment != nil { + notification.NotifyPullRequestPushCommits(ctx, doer, pr, comment) + } + } + if isSync { requests := issues_model.PullRequestList(prs) if err = requests.LoadAttributes(); err != nil { @@ -303,24 +321,6 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } } - for _, pr := range prs { - log.Trace("Updating PR[%d]: composing new test task", pr.ID) - if pr.Flow == issues_model.PullRequestFlowGithub { - if err := PushToBaseRepo(ctx, pr); err != nil { - log.Error("PushToBaseRepo: %v", err) - continue - } - } else { - continue - } - - AddToTaskQueue(pr) - comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) - if err == nil && comment != nil { - notification.NotifyPullRequestPushCommits(ctx, doer, pr, comment) - } - } - log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch) if err != nil { diff --git a/services/pull/update.go b/services/pull/update.go index ede89bcdff..b9525cf0c9 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -106,7 +106,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, BaseBranch: pull.HeadBranch, } - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { return false, false, err } diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 91e4b1df52..a3c94a6cc2 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -361,6 +361,14 @@ +
+ + +
+
+ + +
{{end}} diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl index b44eb799b9..8d199854ae 100644 --- a/templates/admin/auth/source/ldap.tmpl +++ b/templates/admin/auth/source/ldap.tmpl @@ -52,7 +52,7 @@
- +

{{.locale.Tr "admin.auths.restricted_filter_helper"}}

diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index 166373a324..85c77343a5 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -98,4 +98,12 @@
+
+ + +
+
+ + +
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 4d3e08a597..e3cac806a4 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -16,10 +16,13 @@ {{if .EnableCaptcha}} {{if eq .CaptchaType "recaptcha"}} - + {{end}} {{if eq .CaptchaType "hcaptcha"}} - + + {{end}} + {{if eq .CaptchaType "cfturnstile"}} + {{end}} {{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 90a54608e6..1c995e73e7 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -3,22 +3,24 @@ {{if .IsSigned}} {{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}} {{end}} -
+
- {{if .IsSigned}} - - - {{svg "octicon-bell"}} - - {{$notificationUnreadCount}} +
+ {{if .IsSigned}} + + + {{svg "octicon-bell"}} + + {{$notificationUnreadCount}} + - - - {{end}} -
@@ -78,12 +80,10 @@ {{else if .IsSigned}} {{end}} - - - {{svg "octicon-bell"}} - + + + {{svg "octicon-bell"}} + {{$notificationUnreadCount}} - -