Skip to content

Commit 10075b6

Browse files
committed
Merge branch 'main' into enforce-2fa-2
2 parents 18d4861 + d1a3bd6 commit 10075b6

File tree

11 files changed

+170
-69
lines changed

11 files changed

+170
-69
lines changed

cmd/admin_user_create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ var microcmdUserCreate = &cli.Command{
8181
Name: "restricted",
8282
Usage: "Make a restricted user account",
8383
},
84+
&cli.StringFlag{
85+
Name: "fullname",
86+
Usage: `The full, human-readable name of the user`,
87+
},
8488
},
8589
}
8690

@@ -191,6 +195,7 @@ func runCreateUser(c *cli.Context) error {
191195
Passwd: password,
192196
MustChangePassword: mustChangePassword,
193197
Visibility: visibility,
198+
FullName: c.String("fullname"),
194199
}
195200

196201
overwriteDefault := &user_model.CreateUserOverwriteOptions{

cmd/admin_user_create_test.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,17 @@ func TestAdminUserCreate(t *testing.T) {
5050
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
5151
})
5252

53-
createUser := func(name, args string) error {
54-
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
53+
createUser := func(name string, args ...string) error {
54+
return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
5555
}
5656

5757
t.Run("UserType", func(t *testing.T) {
5858
reset()
59-
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
60-
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
61-
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
59+
assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
60+
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
61+
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
6262

63-
assert.NoError(t, createUser("u", "--user-type bot"))
63+
assert.NoError(t, createUser("u", "--user-type", "bot"))
6464
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
6565
assert.Equal(t, user_model.UserTypeBot, u.Type)
6666
assert.Empty(t, u.Passwd)
@@ -75,7 +75,7 @@ func TestAdminUserCreate(t *testing.T) {
7575

7676
// using "--access-token" only means "all" access
7777
reset()
78-
assert.NoError(t, createUser("u", "--random-password --access-token"))
78+
assert.NoError(t, createUser("u", "--random-password", "--access-token"))
7979
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
8080
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
8181
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
@@ -85,7 +85,7 @@ func TestAdminUserCreate(t *testing.T) {
8585

8686
// using "--access-token" with name & scopes
8787
reset()
88-
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
88+
assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
8989
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
9090
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
9191
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
@@ -98,23 +98,38 @@ func TestAdminUserCreate(t *testing.T) {
9898

9999
// using "--access-token-name" without "--access-token"
100100
reset()
101-
err = createUser("u", "--random-password --access-token-name new-token-name")
101+
err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
102102
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
103103
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
104104
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
105105

106106
// using "--access-token-scopes" without "--access-token"
107107
reset()
108-
err = createUser("u", "--random-password --access-token-scopes read:issue")
108+
err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
109109
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
110110
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
111111
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
112112

113113
// empty permission
114114
reset()
115-
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
115+
err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
116116
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
117117
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
118118
assert.ErrorContains(t, err, "access token does not have any permission")
119119
})
120+
121+
t.Run("UserFields", func(t *testing.T) {
122+
reset()
123+
assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
124+
unittest.AssertExistsAndLoadBean(t, &user_model.User{
125+
Name: "u-FullNameWithSpace",
126+
LowerName: "u-fullnamewithspace",
127+
FullName: "First O'Middle Last",
128+
129+
})
130+
131+
assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
132+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
133+
assert.Empty(t, u.FullName)
134+
})
120135
}

custom/conf/app.example.ini

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,27 +59,16 @@ RUN_USER = ; git
5959
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6060
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6161
;;
62-
;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http'
63-
;; Note: Value must be lowercase.
62+
;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
6463
;PROTOCOL = http
6564
;;
66-
;; Expect PROXY protocol headers on connections
67-
;USE_PROXY_PROTOCOL = false
68-
;;
69-
;; Use PROXY protocol in TLS Bridging mode
70-
;PROXY_PROTOCOL_TLS_BRIDGING = false
71-
;;
72-
; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
73-
;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
74-
;;
75-
; Accept PROXY protocol headers with UNKNOWN type
76-
;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
77-
;;
78-
;; Set the domain for the server
65+
;; Set the domain for the server.
66+
;; Most users should set it to the real website domain of their Gitea instance.
7967
;DOMAIN = localhost
8068
;;
8169
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
82-
;; Most users should set it to the real website URL of their Gitea instance.
70+
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
71+
;; When it is empty, Gitea will use HTTP "Host" header to generate ROOT_URL, and fall back to the default one if no "Host" header.
8372
;ROOT_URL =
8473
;;
8574
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
@@ -90,13 +79,25 @@ RUN_USER = ; git
9079
;STATIC_URL_PREFIX =
9180
;;
9281
;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
93-
;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use.
82+
;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use.
9483
;; Relative paths will be made absolute against the _`AppWorkPath`_.
9584
;HTTP_ADDR = 0.0.0.0
9685
;;
97-
;; The port to listen on. Leave empty when using a unix socket.
86+
;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket.
9887
;HTTP_PORT = 3000
9988
;;
89+
;; Expect PROXY protocol headers on connections
90+
;USE_PROXY_PROTOCOL = false
91+
;;
92+
;; Use PROXY protocol in TLS Bridging mode
93+
;PROXY_PROTOCOL_TLS_BRIDGING = false
94+
;;
95+
;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
96+
;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s
97+
;;
98+
;; Accept PROXY protocol headers with UNKNOWN type
99+
;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false
100+
;;
100101
;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server
101102
;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main
102103
;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for
@@ -520,7 +521,7 @@ INTERNAL_TOKEN =
520521
;; RECORD_USER_SIGNUP_METADATA = false
521522
;;
522523
;; Set the two-factor auth behavior.
523-
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to any repositories.
524+
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
524525
;TWO_FACTOR_AUTH =
525526

526527
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/httplib/url.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,16 @@ func GuessCurrentHostURL(ctx context.Context) string {
7070
// 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
7171
// 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx.
7272
// 3. There is no reverse proxy.
73-
// Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3,
74-
// then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users.
75-
// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
73+
// Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
74+
// wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users.
75+
// So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty
7676
reqScheme := getRequestScheme(req)
7777
if reqScheme == "" {
78+
// if no reverse proxy header, try to use "Host" header for absolute URL
79+
if setting.UseHostHeader && req.Host != "" {
80+
return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
81+
}
82+
// fall back to default AppURL
7883
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
7984
}
8085
// X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.

modules/httplib/url_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package httplib
55

66
import (
77
"context"
8+
"crypto/tls"
89
"net/http"
910
"testing"
1011

@@ -39,6 +40,25 @@ func TestIsRelativeURL(t *testing.T) {
3940
}
4041
}
4142

43+
func TestGuessCurrentHostURL(t *testing.T) {
44+
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
45+
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
46+
defer test.MockVariableValue(&setting.UseHostHeader, false)()
47+
48+
ctx := t.Context()
49+
assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
50+
51+
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"})
52+
assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
53+
54+
defer test.MockVariableValue(&setting.UseHostHeader, true)()
55+
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"})
56+
assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx))
57+
58+
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}})
59+
assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx))
60+
}
61+
4262
func TestMakeAbsoluteURL(t *testing.T) {
4363
defer test.MockVariableValue(&setting.Protocol, "http")()
4464
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()

modules/markup/html.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
7171
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
7272
// http://spec.commonmark.org/0.28/#email-address
7373
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
74-
v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
74+
// At the moment, we use stricter rule for rendering purpose: only allow the "name" part starting after the word boundary
75+
v.emailRegex = regexp.MustCompile(`\b([-\w.!#$%&'*+/=?^{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)\b`)
7576

7677
// emojiShortCodeRegex find emoji by alias like :smile:
7778
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)

modules/markup/html_email.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
package markup
55

6-
import "golang.org/x/net/html"
6+
import (
7+
"strings"
8+
9+
"golang.org/x/net/html"
10+
)
711

812
// emailAddressProcessor replaces raw email addresses with a mailto: link.
913
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
@@ -14,6 +18,14 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
1418
return
1519
}
1620

21+
var nextByte byte
22+
if len(node.Data) > m[3] {
23+
nextByte = node.Data[m[3]]
24+
}
25+
if strings.IndexByte(":/", nextByte) != -1 {
26+
// for cases: "[email protected]:owner/repo.git", "https://[email protected]/owner/repo.git"
27+
return
28+
}
1729
mail := node.Data[m[2]:m[3]]
1830
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/))
1931
node = node.NextSibling.NextSibling

modules/markup/html_test.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,10 @@ func TestRender_email(t *testing.T) {
225225
test := func(input, expected string) {
226226
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
227227
assert.NoError(t, err)
228-
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
228+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input)
229229
}
230-
// Text that should be turned into email link
231230

231+
// Text that should be turned into email link
232232
test(
233233
234234
`<p><a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`)
@@ -260,28 +260,48 @@ func TestRender_email(t *testing.T) {
260260
<a href="mailto:[email protected]" rel="nofollow">[email protected]</a>?
261261
<a href="mailto:[email protected]" rel="nofollow">[email protected]</a>!</p>`)
262262

263+
// match GitHub behavior
264+
test("email@[email protected]", `<p>email@<a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`)
265+
266+
// match GitHub behavior
267+
test(`"[email protected]"`, `<p>&#34;<a href="mailto:[email protected]" rel="nofollow">[email protected]</a>&#34;</p>`)
268+
263269
// Test that should *not* be turned into email links
264-
test(
265-
266-
`<p>&#34;[email protected]&#34;</p>`)
267270
test(
268271
"/home/gitea/mailstore/info@gitea/com",
269272
`<p>/home/gitea/mailstore/info@gitea/com</p>`)
270273
test(
271274
"[email protected]:go-gitea/gitea.git",
272275
`<p>[email protected]:go-gitea/gitea.git</p>`)
276+
test(
277+
"https://foo:[email protected]",
278+
`<p><a href="https://foo:[email protected]" rel="nofollow">https://foo:[email protected]</a></p>`)
273279
test(
274280
"gitea@3",
275281
`<p>gitea@3</p>`)
276282
test(
277283
278284
279-
test(
280-
281-
`<p>email@[email protected]</p>`)
282285
test(
283286
284287
288+
289+
cases := []struct {
290+
input, expected string
291+
}{
292+
// match GitHub behavior
293+
{"[email protected]", `<p>?<a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`},
294+
{"*[email protected]", `<p>*<a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`},
295+
{"[email protected]", `<p>~<a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`},
296+
297+
// the following cases don't match GitHub behavior, but they are valid email addresses ...
298+
// maybe we should reduce the candidate characters for the "name" part in the future
299+
{"a*[email protected]", `<p><a href="mailto:a*[email protected]" rel="nofollow">a*[email protected]</a></p>`},
300+
{"[email protected]", `<p><a href="mailto:[email protected]" rel="nofollow">[email protected]</a></p>`},
301+
}
302+
for _, c := range cases {
303+
test(c.input, c.expected)
304+
}
285305
}
286306

287307
func TestRender_emoji(t *testing.T) {

0 commit comments

Comments
 (0)