Skip to content

Commit b7be5b3

Browse files
committed
Merge remote-tracking branch 'upstream/main'
* upstream/main: Pre-register OAuth2 applications for git credential helpers (go-gitea#26291) Make `user-content-* ` consistent with github (go-gitea#26388) Add pull request review request webhook event (go-gitea#26401) Introduce ctx.PathParamRaw to avoid incorrect unescaping (go-gitea#26392)
2 parents b842d2a + 63ab92d commit b7be5b3

File tree

18 files changed

+226
-38
lines changed

18 files changed

+226
-38
lines changed

custom/conf/app.example.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,11 @@ ENABLE = true
544544
;;
545545
;; Maximum length of oauth2 token/cookie stored on server
546546
;MAX_TOKEN_LENGTH = 32767
547+
;;
548+
;; Pre-register OAuth2 applications for some universally useful services
549+
;; * https://github.com/hickford/git-credential-oauth
550+
;; * https://github.com/git-ecosystem/git-credential-manager
551+
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager
547552

548553
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
549554
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/administration/config-cheat-sheet.en-us.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ This section only does "set" config, a removed config key from this section won'
11001100
- `JWT_SECRET_URI`: **_empty_**: Instead of defining JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/oauth2_jwt_secret`)
11011101
- `JWT_SIGNING_PRIVATE_KEY_FILE`: **jwt/private.pem**: Private key file path used to sign OAuth2 tokens. The path is relative to `APP_DATA_PATH`. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `RS256`, `RS384`, `RS512`, `ES256`, `ES384` or `ES512`. The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you.
11021102
- `MAX_TOKEN_LENGTH`: **32767**: Maximum length of token/cookie to accept from OAuth2 provider
1103+
- `DEFAULT_APPLICATIONS`: **git-credential-oauth, git-credential-manager**: Pre-register OAuth applications for some services on startup. See the [OAuth2 documentation](/development/oauth2-provider.md) for the list of available options.
11031104

11041105
## i18n (`i18n`)
11051106

docs/content/development/oauth2-provider.en-us.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ Gitea token scopes are as follows:
7878
|     **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. |
7979
|     **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings. |
8080

81+
## Pre-configured Applications
82+
83+
Gitea creates OAuth applications for the following services by default on startup, as we assume that these are universally useful.
84+
85+
|Application|Description|Client ID|
86+
|-----------|-----------|---------|
87+
|[git-credential-oauth](https://github.com/hickford/git-credential-oauth)|Git credential helper|`a4792ccc-144e-407e-86c9-5e7d8d9c3269`|
88+
|[Git Credential Manager](https://github.com/git-ecosystem/git-credential-manager)|Git credential helper|`e90ee53c-94e2-48ac-9358-a874fb9e0662`|
89+
90+
To prevent unexpected behavior, they are being displayed as locked in the UI and their creation can instead be controlled by the `DEFAULT_APPLICATIONS` parameter in `app.ini`.
91+
8192
## Client types
8293

8394
Gitea supports both confidential and public client types, [as defined by RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1).

models/auth/oauth2.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strings"
1414

1515
"code.gitea.io/gitea/models/db"
16+
"code.gitea.io/gitea/modules/container"
17+
"code.gitea.io/gitea/modules/setting"
1618
"code.gitea.io/gitea/modules/timeutil"
1719
"code.gitea.io/gitea/modules/util"
1820

@@ -46,6 +48,83 @@ func init() {
4648
db.RegisterModel(new(OAuth2Grant))
4749
}
4850

51+
type BuiltinOAuth2Application struct {
52+
ConfigName string
53+
DisplayName string
54+
RedirectURIs []string
55+
}
56+
57+
func BuiltinApplications() map[string]*BuiltinOAuth2Application {
58+
m := make(map[string]*BuiltinOAuth2Application)
59+
m["a4792ccc-144e-407e-86c9-5e7d8d9c3269"] = &BuiltinOAuth2Application{
60+
ConfigName: "git-credential-oauth",
61+
DisplayName: "git-credential-oauth",
62+
RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
63+
}
64+
m["e90ee53c-94e2-48ac-9358-a874fb9e0662"] = &BuiltinOAuth2Application{
65+
ConfigName: "git-credential-manager",
66+
DisplayName: "Git Credential Manager",
67+
RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
68+
}
69+
return m
70+
}
71+
72+
func Init(ctx context.Context) error {
73+
builtinApps := BuiltinApplications()
74+
var builtinAllClientIDs []string
75+
for clientID := range builtinApps {
76+
builtinAllClientIDs = append(builtinAllClientIDs, clientID)
77+
}
78+
79+
var registeredApps []*OAuth2Application
80+
if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
81+
return err
82+
}
83+
84+
clientIDsToAdd := container.Set[string]{}
85+
for _, configName := range setting.OAuth2.DefaultApplications {
86+
found := false
87+
for clientID, builtinApp := range builtinApps {
88+
if builtinApp.ConfigName == configName {
89+
clientIDsToAdd.Add(clientID) // add all user-configured apps to the "add" list
90+
found = true
91+
}
92+
}
93+
if !found {
94+
return fmt.Errorf("unknown oauth2 application: %q", configName)
95+
}
96+
}
97+
clientIDsToDelete := container.Set[string]{}
98+
for _, app := range registeredApps {
99+
if !clientIDsToAdd.Contains(app.ClientID) {
100+
clientIDsToDelete.Add(app.ClientID) // if a registered app is not in the "add" list, it should be deleted
101+
}
102+
}
103+
for _, app := range registeredApps {
104+
clientIDsToAdd.Remove(app.ClientID) // no need to re-add existing (registered) apps, so remove them from the set
105+
}
106+
107+
for _, app := range registeredApps {
108+
if clientIDsToDelete.Contains(app.ClientID) {
109+
if err := deleteOAuth2Application(ctx, app.ID, 0); err != nil {
110+
return err
111+
}
112+
}
113+
}
114+
for clientID := range clientIDsToAdd {
115+
builtinApp := builtinApps[clientID]
116+
if err := db.Insert(ctx, &OAuth2Application{
117+
Name: builtinApp.DisplayName,
118+
ClientID: clientID,
119+
RedirectURIs: builtinApp.RedirectURIs,
120+
}); err != nil {
121+
return err
122+
}
123+
}
124+
125+
return nil
126+
}
127+
49128
// TableName sets the table name to `oauth2_application`
50129
func (app *OAuth2Application) TableName() string {
51130
return "oauth2_application"
@@ -205,6 +284,10 @@ func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) (*OAuth2Applic
205284
if app.UID != opts.UserID {
206285
return nil, fmt.Errorf("UID mismatch")
207286
}
287+
builtinApps := BuiltinApplications()
288+
if _, builtin := builtinApps[app.ClientID]; builtin {
289+
return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID)
290+
}
208291

209292
app.Name = opts.Name
210293
app.RedirectURIs = opts.RedirectURIs
@@ -261,6 +344,14 @@ func DeleteOAuth2Application(id, userid int64) error {
261344
return err
262345
}
263346
defer committer.Close()
347+
app, err := GetOAuth2ApplicationByID(ctx, id)
348+
if err != nil {
349+
return err
350+
}
351+
builtinApps := BuiltinApplications()
352+
if _, builtin := builtinApps[app.ClientID]; builtin {
353+
return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID)
354+
}
264355
if err := deleteOAuth2Application(ctx, id, userid); err != nil {
265356
return err
266357
}

modules/context/base.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ func (b *Base) Params(p string) string {
147147
return s
148148
}
149149

150+
func (b *Base) PathParamRaw(p string) string {
151+
return chi.URLParam(b.Req, strings.TrimPrefix(p, ":"))
152+
}
153+
150154
// ParamsInt64 returns the param on route as int64
151155
func (b *Base) ParamsInt64(p string) int64 {
152156
v, _ := strconv.ParseInt(b.Params(p), 10, 64)

modules/markup/common/footnote.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,12 @@ func CleanValue(value []byte) []byte {
2929
value = bytes.TrimSpace(value)
3030
rs := bytes.Runes(value)
3131
result := make([]rune, 0, len(rs))
32-
needsDash := false
3332
for _, r := range rs {
34-
switch {
35-
case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_':
36-
if needsDash && len(result) > 0 {
37-
result = append(result, '-')
38-
}
39-
needsDash = false
33+
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
4034
result = append(result, unicode.ToLower(r))
41-
default:
42-
needsDash = true
35+
}
36+
if unicode.IsSpace(r) {
37+
result = append(result, '-')
4338
}
4439
}
4540
return []byte(string(result))
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
package common
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestCleanValue(t *testing.T) {
12+
tests := []struct {
13+
param string
14+
expect string
15+
}{
16+
// Github behavior test cases
17+
{"", ""},
18+
{"test(0)", "test0"},
19+
{"test!1", "test1"},
20+
{"test:2", "test2"},
21+
{"test*3", "test3"},
22+
{"test!4", "test4"},
23+
{"test:5", "test5"},
24+
{"test*6", "test6"},
25+
{"test:6 a", "test6-a"},
26+
{"test:6 !b", "test6-b"},
27+
{"test:ad # df", "testad--df"},
28+
{"test:ad #23 df 2*/*", "testad-23-df-2"},
29+
{"test:ad 23 df 2*/*", "testad-23-df-2"},
30+
{"test:ad # 23 df 2*/*", "testad--23-df-2"},
31+
{"Anchors in Markdown", "anchors-in-markdown"},
32+
{"a_b_c", "a_b_c"},
33+
{"a-b-c", "a-b-c"},
34+
{"a-b-c----", "a-b-c----"},
35+
{"test:6a", "test6a"},
36+
{"test:a6", "testa6"},
37+
{"tes a a a a", "tes-a-a---a--a"},
38+
{" tes a a a a ", "tes-a-a---a--a"},
39+
{"Header with \"double quotes\"", "header-with-double-quotes"},
40+
{"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-links-click"},
41+
{"tes()", "tes"},
42+
{"tes(0)", "tes0"},
43+
{"tes{0}", "tes0"},
44+
{"tes[0]", "tes0"},
45+
{"test【0】", "test0"},
46+
{"tes…@a", "tesa"},
47+
{"tes¥& a", "tes-a"},
48+
{"tes= a", "tes-a"},
49+
{"tes|a", "tesa"},
50+
{"tes\\a", "tesa"},
51+
{"tes/a", "tesa"},
52+
{"a啊啊b", "a啊啊b"},
53+
{"c🤔️🤔️d", "cd"},
54+
{"a⚡a", "aa"},
55+
{"e.~f", "ef"},
56+
}
57+
for _, test := range tests {
58+
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
59+
}
60+
}

modules/setting/oauth2.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ var OAuth2 = struct {
100100
JWTSecretBase64 string `ini:"JWT_SECRET"`
101101
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
102102
MaxTokenLength int
103+
DefaultApplications []string
103104
}{
104105
Enable: true,
105106
AccessTokenExpirationTime: 3600,
@@ -108,6 +109,7 @@ var OAuth2 = struct {
108109
JWTSigningAlgorithm: "RS256",
109110
JWTSigningPrivateKeyFile: "jwt/private.pem",
110111
MaxTokenLength: math.MaxInt16,
112+
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager"},
111113
}
112114

113115
func loadOAuth2From(rootCfg ConfigProvider) {

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ edit = Edit
9393

9494
enabled = Enabled
9595
disabled = Disabled
96+
locked = Locked
9697

9798
copy = Copy
9899
copy_url = Copy URL
@@ -850,6 +851,7 @@ oauth2_client_secret_hint = The secret will not be shown again after you leave o
850851
oauth2_application_edit = Edit
851852
oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance.
852853
oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?
854+
oauth2_application_locked = Gitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected bahavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.
853855

854856
authorized_oauth2_applications = Authorized OAuth2 Applications
855857
authorized_oauth2_applications_description = You have granted access to your personal Gitea account to these third party applications. Please revoke access for applications you no longer need.

routers/api/v1/repo/wiki.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func EditWikiPage(ctx *context.APIContext) {
127127

128128
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
129129

130-
oldWikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName"))
130+
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
131131
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
132132

133133
if len(newWikiName) == 0 {
@@ -231,7 +231,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
231231
// "404":
232232
// "$ref": "#/responses/notFound"
233233

234-
wikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName"))
234+
wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
235235

236236
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
237237
if err.Error() == "file does not exist" {
@@ -359,7 +359,7 @@ func GetWikiPage(ctx *context.APIContext) {
359359
// "$ref": "#/responses/notFound"
360360

361361
// get requested pagename
362-
pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName"))
362+
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
363363

364364
wikiPage := getWikiPage(ctx, pageName)
365365
if !ctx.Written() {
@@ -409,7 +409,7 @@ func ListPageRevisions(ctx *context.APIContext) {
409409
}
410410

411411
// get requested pagename
412-
pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName"))
412+
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
413413
if len(pageName) == 0 {
414414
pageName = "Home"
415415
}

0 commit comments

Comments
 (0)