Skip to content

Commit fb168b8

Browse files
authored
Merge branch 'main' into refactor-markup
2 parents 9aa32ae + 87bb5ed commit fb168b8

33 files changed

+301
-239
lines changed

models/auth/webauthn.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/modules/timeutil"
1313
"code.gitea.io/gitea/modules/util"
1414

15+
"github.com/go-webauthn/webauthn/protocol"
1516
"github.com/go-webauthn/webauthn/webauthn"
1617
)
1718

@@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
8990
// WebAuthnCredentialList is a list of *WebAuthnCredential
9091
type WebAuthnCredentialList []*WebAuthnCredential
9192

93+
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
94+
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
95+
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
96+
return webauthn.CredentialFlags{
97+
UserPresent: flags.HasUserPresent(),
98+
UserVerified: flags.HasUserVerified(),
99+
BackupEligible: flags.HasBackupEligible(),
100+
BackupState: flags.HasBackupState(),
101+
}
102+
}
103+
92104
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
93-
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
105+
func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
106+
// TODO: at the moment, Gitea doesn't store or check the flags
107+
// so we need to use the default flags from the authenticator to make the login validation pass
108+
// In the future, we should:
109+
// 1. store the flags when registering the credential
110+
// 2. provide the stored flags when converting the credentials (for login)
111+
// 3. for old users, still use this fallback to the default flags
112+
defAuthFlags := util.OptionalArg(defaultAuthFlags)
94113
creds := make([]webauthn.Credential, 0, len(list))
95114
for _, cred := range list {
96115
creds = append(creds, webauthn.Credential{
97116
ID: cred.CredentialID,
98117
PublicKey: cred.PublicKey,
99118
AttestationType: cred.AttestationType,
119+
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
100120
Authenticator: webauthn.Authenticator{
101121
AAGUID: cred.AAGUID,
102122
SignCount: cred.SignCount,

models/db/engine.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ func SyncAllTables() error {
134134
func InitEngine(ctx context.Context) error {
135135
xormEngine, err := newXORMEngine()
136136
if err != nil {
137+
if strings.Contains(err.Error(), "SQLite3 support") {
138+
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
139+
}
137140
return fmt.Errorf("failed to connect to database: %w", err)
138141
}
139142

models/migrations/base/tests.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
"code.gitea.io/gitea/modules/setting"
1919
"code.gitea.io/gitea/modules/testlogger"
2020

21-
"github.com/stretchr/testify/assert"
21+
"github.com/stretchr/testify/require"
2222
"xorm.io/xorm"
2323
)
2424

@@ -33,15 +33,15 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
3333
ourSkip := 2
3434
ourSkip += skip
3535
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
36-
assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
36+
require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
3737

3838
if err := deleteDB(); err != nil {
39-
t.Errorf("unable to reset database: %v", err)
39+
t.Fatalf("unable to reset database: %v", err)
4040
return nil, deferFn
4141
}
4242

4343
x, err := newXORMEngine()
44-
assert.NoError(t, err)
44+
require.NoError(t, err)
4545
if x != nil {
4646
oldDefer := deferFn
4747
deferFn = func() {

modules/auth/webauthn/webauthn.go

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
package webauthn
55

66
import (
7+
"context"
78
"encoding/binary"
89
"encoding/gob"
910

1011
"code.gitea.io/gitea/models/auth"
11-
"code.gitea.io/gitea/models/db"
1212
user_model "code.gitea.io/gitea/models/user"
1313
"code.gitea.io/gitea/modules/setting"
14+
"code.gitea.io/gitea/modules/util"
1415

1516
"github.com/go-webauthn/webauthn/protocol"
1617
"github.com/go-webauthn/webauthn/webauthn"
@@ -38,40 +39,42 @@ func Init() {
3839
}
3940
}
4041

41-
// User represents an implementation of webauthn.User based on User model
42-
type User user_model.User
42+
// user represents an implementation of webauthn.User based on User model
43+
type user struct {
44+
ctx context.Context
45+
User *user_model.User
46+
47+
defaultAuthFlags protocol.AuthenticatorFlags
48+
}
49+
50+
var _ webauthn.User = (*user)(nil)
51+
52+
func NewWebAuthnUser(ctx context.Context, u *user_model.User, defaultAuthFlags ...protocol.AuthenticatorFlags) webauthn.User {
53+
return &user{ctx: ctx, User: u, defaultAuthFlags: util.OptionalArg(defaultAuthFlags)}
54+
}
4355

4456
// WebAuthnID implements the webauthn.User interface
45-
func (u *User) WebAuthnID() []byte {
57+
func (u *user) WebAuthnID() []byte {
4658
id := make([]byte, 8)
47-
binary.PutVarint(id, u.ID)
59+
binary.PutVarint(id, u.User.ID)
4860
return id
4961
}
5062

5163
// WebAuthnName implements the webauthn.User interface
52-
func (u *User) WebAuthnName() string {
53-
if u.LoginName == "" {
54-
return u.Name
55-
}
56-
return u.LoginName
64+
func (u *user) WebAuthnName() string {
65+
return util.IfZero(u.User.LoginName, u.User.Name)
5766
}
5867

5968
// WebAuthnDisplayName implements the webauthn.User interface
60-
func (u *User) WebAuthnDisplayName() string {
61-
return (*user_model.User)(u).DisplayName()
62-
}
63-
64-
// WebAuthnIcon implements the webauthn.User interface
65-
func (u *User) WebAuthnIcon() string {
66-
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
69+
func (u *user) WebAuthnDisplayName() string {
70+
return u.User.DisplayName()
6771
}
6872

6973
// WebAuthnCredentials implements the webauthn.User interface
70-
func (u *User) WebAuthnCredentials() []webauthn.Credential {
71-
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID)
74+
func (u *user) WebAuthnCredentials() []webauthn.Credential {
75+
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
7276
if err != nil {
7377
return nil
7478
}
75-
76-
return dbCreds.ToCredentials()
79+
return dbCreds.ToCredentials(u.defaultAuthFlags)
7780
}

routers/web/admin/emails.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func ActivateEmail(ctx *context.Context) {
154154

155155
// DeleteEmail serves a POST request for delete a user's email
156156
func DeleteEmail(ctx *context.Context) {
157-
u, err := user_model.GetUserByID(ctx, ctx.FormInt64("Uid"))
157+
u, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
158158
if err != nil || u == nil {
159159
ctx.ServerError("GetUserByID", err)
160160
return

routers/web/auth/webauthn.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,17 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
7676
}()
7777

7878
// Validate the parsed response.
79+
80+
// ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
7981
var user *user_model.User
80-
cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
82+
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
83+
if err != nil {
84+
// Failed authentication attempt.
85+
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
86+
ctx.Status(http.StatusForbidden)
87+
return
88+
}
89+
cred, err := wa.WebAuthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
8190
userID, n := binary.Varint(userHandle)
8291
if n <= 0 {
8392
return nil, errors.New("invalid rawID")
@@ -89,8 +98,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
8998
return nil, err
9099
}
91100

92-
return (*wa.User)(user), nil
93-
}, *sessionData, ctx.Req)
101+
return wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags), nil
102+
}, *sessionData, parsedResponse)
94103
if err != nil {
95104
// Failed authentication attempt.
96105
log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
@@ -171,7 +180,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
171180
return
172181
}
173182

174-
assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
183+
webAuthnUser := wa.NewWebAuthnUser(ctx, user)
184+
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
175185
if err != nil {
176186
ctx.ServerError("webauthn.BeginLogin", err)
177187
return
@@ -216,7 +226,8 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
216226
}
217227

218228
// Validate the parsed response.
219-
cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
229+
webAuthnUser := wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags)
230+
cred, err := wa.WebAuthn.ValidateLogin(webAuthnUser, *sessionData, parsedResponse)
220231
if err != nil {
221232
// Failed authentication attempt.
222233
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)

routers/web/devtest/devtest.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ func List(ctx *context.Context) {
2424
var subNames []string
2525
for _, tmplName := range templateNames {
2626
subName := strings.TrimSuffix(tmplName, ".tmpl")
27-
if subName != "list" {
27+
if !strings.HasPrefix(subName, "devtest-") {
2828
subNames = append(subNames, subName)
2929
}
3030
}
3131
ctx.Data["SubNames"] = subNames
32-
ctx.HTML(http.StatusOK, "devtest/list")
32+
ctx.HTML(http.StatusOK, "devtest/devtest-list")
3333
}
3434

3535
func FetchActionTest(ctx *context.Context) {

routers/web/user/setting/security/webauthn.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ func WebAuthnRegister(ctx *context.Context) {
5151
return
5252
}
5353

54-
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer), webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
54+
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
55+
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
5556
ResidentKey: protocol.ResidentKeyRequirementRequired,
5657
}))
5758
if err != nil {
@@ -92,7 +93,8 @@ func WebauthnRegisterPost(ctx *context.Context) {
9293
}()
9394

9495
// Verify that the challenge succeeded
95-
cred, err := wa.WebAuthn.FinishRegistration((*wa.User)(ctx.Doer), *sessionData, ctx.Req)
96+
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
97+
cred, err := wa.WebAuthn.FinishRegistration(webAuthnUser, *sessionData, ctx.Req)
9698
if err != nil {
9799
if pErr, ok := err.(*protocol.Error); ok {
98100
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)

templates/admin/emails/list.tmpl

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,21 @@
5050
<td>{{svg (Iif .IsPrimary "octicon-check" "octicon-x")}}</td>
5151
<td>
5252
{{if .CanChange}}
53-
<a class="link-email-action" href data-uid="{{.UID}}"
54-
data-email="{{.Email}}"
55-
data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
56-
data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
53+
<a class="show-modal" href data-modal="#change-email-modal" data-modal-uid="{{.UID}}"
54+
data-modal-email="{{.Email}}"
55+
data-modal-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
56+
data-modal-activate="{{if .IsActivated}}0{{else}}1{{end}}">
5757
{{svg (Iif .IsActivated "octicon-check" "octicon-x")}}
5858
</a>
5959
{{else}}
6060
{{svg (Iif .IsActivated "octicon-check" "octicon-x")}}
6161
{{end}}
6262
</td>
6363
<td>
64-
<div class="tw-flex tw-gap-2">
65-
<a class="delete-button" href="" data-url="{{$.Link}}/delete" data-id="{{.ID}}" data-data-uid="{{.UID}}">{{svg "octicon-trash"}}</a>
66-
</div>
64+
<a class="link-action negative" href data-url="{{$.Link}}/delete?id={{.ID}}&uid={{.UID}}"
65+
data-modal-confirm-header="{{ctx.Locale.Tr "admin.emails.delete"}}"
66+
data-modal-confirm-content="{{ctx.Locale.Tr "admin.emails.delete_desc"}}"
67+
>{{svg "octicon-trash"}}</a>
6768
</td>
6869
</tr>
6970
{{end}}
@@ -77,40 +78,24 @@
7778
<div class="header">
7879
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
7980
</div>
80-
<div class="content">
81+
<form class="content ui form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post">
8182
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
8283

83-
<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post">
84-
{{$.CsrfTokenHtml}}
84+
{{$.CsrfTokenHtml}}
8585

86-
<input type="hidden" id="query-sort" name="sort" value="{{.SortType}}">
87-
<input type="hidden" id="query-keyword" name="q" value="{{.Keyword}}">
88-
<input type="hidden" id="query-primary" name="is_primary" value="{{.IsPrimary}}" required>
89-
<input type="hidden" id="query-activated" name="is_activated" value="{{.IsActivated}}" required>
86+
<input type="hidden" name="sort" value="{{.SortType}}">
87+
<input type="hidden" name="q" value="{{.Keyword}}">
88+
<input type="hidden" name="is_primary" value="{{.IsPrimary}}">
89+
<input type="hidden" name="is_activated" value="{{.IsActivated}}">
9090

91-
<input type="hidden" id="form-uid" name="uid" value="" required>
92-
<input type="hidden" id="form-email" name="email" value="" required>
93-
<input type="hidden" id="form-primary" name="primary" value="" required>
94-
<input type="hidden" id="form-activate" name="activate" value="" required>
91+
<input type="hidden" name="uid">
92+
<input type="hidden" name="email">
93+
<input type="hidden" name="primary">
94+
<input type="hidden" name="activate">
9595

96-
<div class="center">
97-
{{template "base/modal_actions_confirm" .}}
98-
</div>
99-
</form>
100-
</div>
96+
{{template "base/modal_actions_confirm" .}}
97+
</form>
10198
</div>
102-
103-
</div>
104-
105-
<div class="ui g-modal-confirm delete modal">
106-
<div class="header">
107-
{{svg "octicon-trash"}}
108-
{{ctx.Locale.Tr "admin.emails.delete"}}
109-
</div>
110-
<div class="content">
111-
{{ctx.Locale.Tr "admin.emails.delete_desc"}}
11299
</div>
113-
{{template "base/modal_actions_confirm" .}}
114-
</div>
115100

116101
{{template "admin/layout_footer" .}}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}}
2+
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
3+
{{template "base/footer" dict}}

0 commit comments

Comments
 (0)