+4
custom/conf/app.example.ini
+4
custom/conf/app.example.ini
···
1727
1727
;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address.
1728
1728
;ENVELOPE_FROM =
1729
1729
;;
1730
+
;; If gitea sends mails on behave of users, it will just use the name also displayed in the WebUI. If you want e.g. `Mister X (by CodeIt) <gitea@codeit.net>`,
1731
+
;; set it to `{{ .DisplayName }} (by {{ .AppName }})`. Available Variables: `.DisplayName`, `.AppName` and `.Domain`.
1732
+
;FROM_DISPLAY_NAME_FORMAT = {{ .DisplayName }}
1733
+
;;
1730
1734
;; Mailer user name and password, if required by provider.
1731
1735
;USER =
1732
1736
;;
+1
-1
models/auth/source.go
+1
-1
models/auth/source.go
+5
-1
models/migrations/v1_21/v279.go
+5
-1
models/migrations/v1_21/v279.go
+5
-1
models/migrations/v1_22/v284.go
+5
-1
models/migrations/v1_22/v284.go
···
10
10
type ProtectedBranch struct {
11
11
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
12
12
}
13
-
return x.Sync(new(ProtectedBranch))
13
+
_, err := x.SyncWithOptions(xorm.SyncOptions{
14
+
IgnoreIndices: true,
15
+
IgnoreConstrains: true,
16
+
}, new(ProtectedBranch))
17
+
return err
14
18
}
+5
-1
models/migrations/v1_22/v285.go
+5
-1
models/migrations/v1_22/v285.go
+4
-1
models/migrations/v1_22/v286.go
+4
-1
models/migrations/v1_22/v286.go
···
54
54
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
55
55
}
56
56
57
-
if err := x.Sync(new(Repository)); err != nil {
57
+
if _, err := x.SyncWithOptions(xorm.SyncOptions{
58
+
IgnoreIndices: true,
59
+
IgnoreConstrains: true,
60
+
}, new(Repository)); err != nil {
58
61
return err
59
62
}
60
63
+4
-1
models/migrations/v1_22/v289.go
+4
-1
models/migrations/v1_22/v289.go
···
10
10
ID int64
11
11
DefaultWikiBranch string
12
12
}
13
-
if err := x.Sync(&Repository{}); err != nil {
13
+
if _, err := x.SyncWithOptions(xorm.SyncOptions{
14
+
IgnoreIndices: true,
15
+
IgnoreConstrains: true,
16
+
}, &Repository{}); err != nil {
14
17
return err
15
18
}
16
19
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
+8
-1
models/migrations/v1_22/v290.go
+8
-1
models/migrations/v1_22/v290.go
···
35
35
36
36
func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
37
37
// create missing column
38
-
return x.Sync(new(HookTask))
38
+
if _, err := x.SyncWithOptions(xorm.SyncOptions{
39
+
IgnoreIndices: true,
40
+
IgnoreConstrains: true,
41
+
}, new(HookTask)); err != nil {
42
+
return err
43
+
}
44
+
_, err := x.Exec("UPDATE hook_task SET payload_version = 1 WHERE payload_version IS NULL")
45
+
return err
39
46
}
+5
-1
models/migrations/v1_22/v291.go
+5
-1
models/migrations/v1_22/v291.go
+38
-3
models/user/external_login_user.go
+38
-3
models/user/external_login_user.go
···
160
160
return err
161
161
}
162
162
163
+
// EnsureLinkExternalToUser link the external user to the user
164
+
func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error {
165
+
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
166
+
"external_id": external.ExternalID,
167
+
"login_source_id": external.LoginSourceID,
168
+
})
169
+
if err != nil {
170
+
return err
171
+
}
172
+
173
+
if has {
174
+
_, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external)
175
+
return err
176
+
}
177
+
178
+
_, err = db.GetEngine(ctx).Insert(external)
179
+
return err
180
+
}
181
+
163
182
// FindExternalUserOptions represents an options to find external users
164
183
type FindExternalUserOptions struct {
165
184
db.ListOptions
166
-
Provider string
167
-
UserID int64
168
-
OrderBy string
185
+
Provider string
186
+
UserID int64
187
+
LoginSourceID int64
188
+
HasRefreshToken bool
189
+
Expired bool
190
+
OrderBy string
169
191
}
170
192
171
193
func (opts FindExternalUserOptions) ToConds() builder.Cond {
···
176
198
if opts.UserID > 0 {
177
199
cond = cond.And(builder.Eq{"user_id": opts.UserID})
178
200
}
201
+
if opts.Expired {
202
+
cond = cond.And(builder.Lt{"expires_at": time.Now()})
203
+
}
204
+
if opts.HasRefreshToken {
205
+
cond = cond.And(builder.Neq{"refresh_token": ""})
206
+
}
207
+
if opts.LoginSourceID != 0 {
208
+
cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
209
+
}
179
210
return cond
180
211
}
181
212
182
213
func (opts FindExternalUserOptions) ToOrders() string {
183
214
return opts.OrderBy
184
215
}
216
+
217
+
func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error {
218
+
return db.Iterate(ctx, opts.ToConds(), f)
219
+
}
+10
-1
modules/issue/template/template.go
+10
-1
modules/issue/template/template.go
···
88
88
if err := validateBoolItem(position, field.Attributes, "multiple"); err != nil {
89
89
return err
90
90
}
91
+
if err := validateBoolItem(position, field.Attributes, "list"); err != nil {
92
+
return err
93
+
}
91
94
if err := validateOptions(field, idx); err != nil {
92
95
return err
93
96
}
···
340
343
}
341
344
}
342
345
if len(checkeds) > 0 {
343
-
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
346
+
if list, ok := f.Attributes["list"].(bool); ok && list {
347
+
for _, check := range checkeds {
348
+
_, _ = fmt.Fprintf(builder, "- %s\n", check)
349
+
}
350
+
} else {
351
+
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
352
+
}
344
353
} else {
345
354
_, _ = fmt.Fprint(builder, blankPlaceholder)
346
355
}
+38
-5
modules/issue/template/template_test.go
+38
-5
modules/issue/template/template_test.go
···
217
217
wantErr: "body[0](dropdown): 'multiple' should be a bool",
218
218
},
219
219
{
220
+
name: "dropdown invalid list",
221
+
content: `
222
+
name: "test"
223
+
about: "this is about"
224
+
body:
225
+
- type: "dropdown"
226
+
id: "1"
227
+
attributes:
228
+
label: "a"
229
+
list: "on"
230
+
`,
231
+
wantErr: "body[0](dropdown): 'list' should be a bool",
232
+
},
233
+
{
220
234
name: "checkboxes invalid description",
221
235
content: `
222
236
name: "test"
···
807
821
- type: dropdown
808
822
id: id5
809
823
attributes:
810
-
label: Label of dropdown
824
+
label: Label of dropdown (one line)
825
+
description: Description of dropdown
826
+
multiple: true
827
+
options:
828
+
- Option 1 of dropdown
829
+
- Option 2 of dropdown
830
+
- Option 3 of dropdown
831
+
validations:
832
+
required: true
833
+
- type: dropdown
834
+
id: id6
835
+
attributes:
836
+
label: Label of dropdown (list)
811
837
description: Description of dropdown
812
838
multiple: true
839
+
list: true
813
840
options:
814
841
- Option 1 of dropdown
815
842
- Option 2 of dropdown
···
817
844
validations:
818
845
required: true
819
846
- type: checkboxes
820
-
id: id6
847
+
id: id7
821
848
attributes:
822
849
label: Label of checkboxes
823
850
description: Description of checkboxes
···
836
863
"form-field-id3": {"Value of id3"},
837
864
"form-field-id4": {"Value of id4"},
838
865
"form-field-id5": {"0,1"},
839
-
"form-field-id6-0": {"on"},
840
-
"form-field-id6-2": {"on"},
866
+
"form-field-id6": {"1,2"},
867
+
"form-field-id7-0": {"on"},
868
+
"form-field-id7-2": {"on"},
841
869
},
842
870
},
843
871
···
849
877
850
878
Value of id4
851
879
852
-
### Label of dropdown
880
+
### Label of dropdown (one line)
853
881
854
882
Option 1 of dropdown, Option 2 of dropdown
883
+
884
+
### Label of dropdown (list)
885
+
886
+
- Option 2 of dropdown
887
+
- Option 3 of dropdown
855
888
856
889
### Label of checkboxes
857
890
+15
modules/setting/mailer.go
+15
modules/setting/mailer.go
···
8
8
"net"
9
9
"net/mail"
10
10
"strings"
11
+
"text/template"
11
12
"time"
12
13
13
14
"code.gitea.io/gitea/modules/log"
···
46
47
SendmailArgs []string `ini:"-"`
47
48
SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
48
49
SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
50
+
51
+
// Customization
52
+
FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
53
+
FromDisplayNameFormatTemplate *template.Template `ini:"-"`
49
54
}
50
55
51
56
// MailService the global mailer
···
232
237
MailService.FromEmail = parsed.Address
233
238
} else {
234
239
log.Error("no mailer.FROM provided, email system may not work.")
240
+
}
241
+
242
+
MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
243
+
if MailService.FromDisplayNameFormat != "" {
244
+
template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
245
+
if err != nil {
246
+
log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
247
+
} else {
248
+
MailService.FromDisplayNameFormatTemplate = template
249
+
}
235
250
}
236
251
237
252
switch MailService.EnvelopeFrom {
+3
release-notes/4607.md
+3
release-notes/4607.md
···
1
+
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/21fdd28f084e7f1aef309c9ebd7599ffa6986453) allow synchronizing user status from OAuth2 login providers.
2
+
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/004cc6dc0ab7cc9c324ccb4ecd420c6aeeb20500) add option to change mail from user display name.
3
+
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/d0227c236aa195bd03990210f968b8e52eb20b79) issue Templates: add option to have dropdown printed list.
+2
-4
routers/web/auth/auth.go
+2
-4
routers/web/auth/auth.go
···
619
619
notify_service.NewUserSignUp(ctx, u)
620
620
// update external user information
621
621
if gothUser != nil {
622
-
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
623
-
if !errors.Is(err, util.ErrNotExist) {
624
-
log.Error("UpdateExternalUser failed: %v", err)
625
-
}
622
+
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
623
+
log.Error("EnsureLinkExternalToUser failed: %v", err)
626
624
}
627
625
}
628
626
+31
-33
routers/web/auth/oauth.go
+31
-33
routers/web/auth/oauth.go
···
1154
1154
1155
1155
groups := getClaimedGroups(oauth2Source, &gothUser)
1156
1156
1157
+
opts := &user_service.UpdateOptions{}
1158
+
1159
+
// Reactivate user if they are deactivated
1160
+
if !u.IsActive {
1161
+
opts.IsActive = optional.Some(true)
1162
+
}
1163
+
1164
+
// Update GroupClaims
1165
+
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
1166
+
1167
+
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
1168
+
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
1169
+
ctx.ServerError("SyncGroupsToTeams", err)
1170
+
return
1171
+
}
1172
+
}
1173
+
1174
+
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
1175
+
ctx.ServerError("EnsureLinkExternalToUser", err)
1176
+
return
1177
+
}
1178
+
1157
1179
// If this user is enrolled in 2FA and this source doesn't override it,
1158
1180
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
1159
1181
if !needs2FA {
1182
+
// Register last login
1183
+
opts.SetLastLogin = true
1184
+
1185
+
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
1186
+
ctx.ServerError("UpdateUser", err)
1187
+
return
1188
+
}
1189
+
1160
1190
if err := updateSession(ctx, nil, map[string]any{
1161
1191
"uid": u.ID,
1162
1192
}); err != nil {
···
1167
1197
// Clear whatever CSRF cookie has right now, force to generate a new one
1168
1198
ctx.Csrf.DeleteCookie(ctx)
1169
1199
1170
-
opts := &user_service.UpdateOptions{
1171
-
SetLastLogin: true,
1172
-
}
1173
-
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
1174
-
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
1175
-
ctx.ServerError("UpdateUser", err)
1176
-
return
1177
-
}
1178
-
1179
-
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
1180
-
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
1181
-
ctx.ServerError("SyncGroupsToTeams", err)
1182
-
return
1183
-
}
1184
-
}
1185
-
1186
-
// update external user information
1187
-
if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
1188
-
if !errors.Is(err, util.ErrNotExist) {
1189
-
log.Error("UpdateExternalUser failed: %v", err)
1190
-
}
1191
-
}
1192
-
1193
1200
if err := resetLocale(ctx, u); err != nil {
1194
1201
ctx.ServerError("resetLocale", err)
1195
1202
return
···
1205
1212
return
1206
1213
}
1207
1214
1208
-
opts := &user_service.UpdateOptions{}
1209
-
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
1210
-
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
1215
+
if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
1211
1216
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
1212
1217
ctx.ServerError("UpdateUser", err)
1213
-
return
1214
-
}
1215
-
}
1216
-
1217
-
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
1218
-
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
1219
-
ctx.ServerError("SyncGroupsToTeams", err)
1220
1218
return
1221
1219
}
1222
1220
}
+14
services/auth/source/oauth2/main_test.go
+14
services/auth/source/oauth2/main_test.go
···
1
+
// Copyright 2024 The Gitea Authors. All rights reserved.
2
+
// SPDX-License-Identifier: MIT
3
+
4
+
package oauth2
5
+
6
+
import (
7
+
"testing"
8
+
9
+
"code.gitea.io/gitea/models/unittest"
10
+
)
11
+
12
+
func TestMain(m *testing.M) {
13
+
unittest.MainTest(m, &unittest.TestOptions{})
14
+
}
+62
services/auth/source/oauth2/providers_test.go
+62
services/auth/source/oauth2/providers_test.go
···
1
+
// Copyright 2024 The Gitea Authors. All rights reserved.
2
+
// SPDX-License-Identifier: MIT
3
+
4
+
package oauth2
5
+
6
+
import (
7
+
"time"
8
+
9
+
"github.com/markbates/goth"
10
+
"golang.org/x/oauth2"
11
+
)
12
+
13
+
type fakeProvider struct{}
14
+
15
+
func (p *fakeProvider) Name() string {
16
+
return "fake"
17
+
}
18
+
19
+
func (p *fakeProvider) SetName(name string) {}
20
+
21
+
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) {
22
+
return nil, nil
23
+
}
24
+
25
+
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) {
26
+
return nil, nil
27
+
}
28
+
29
+
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) {
30
+
return goth.User{}, nil
31
+
}
32
+
33
+
func (p *fakeProvider) Debug(bool) {
34
+
}
35
+
36
+
func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
37
+
switch refreshToken {
38
+
case "expired":
39
+
return nil, &oauth2.RetrieveError{
40
+
ErrorCode: "invalid_grant",
41
+
}
42
+
default:
43
+
return &oauth2.Token{
44
+
AccessToken: "token",
45
+
TokenType: "Bearer",
46
+
RefreshToken: "refresh",
47
+
Expiry: time.Now().Add(time.Hour),
48
+
}, nil
49
+
}
50
+
}
51
+
52
+
func (p *fakeProvider) RefreshTokenAvailable() bool {
53
+
return true
54
+
}
55
+
56
+
func init() {
57
+
RegisterGothProvider(
58
+
NewSimpleProvider("fake", "Fake", []string{"account"},
59
+
func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider {
60
+
return &fakeProvider{}
61
+
}))
62
+
}
+1
-1
services/auth/source/oauth2/source.go
+1
-1
services/auth/source/oauth2/source.go
+114
services/auth/source/oauth2/source_sync.go
+114
services/auth/source/oauth2/source_sync.go
···
1
+
// Copyright 2024 The Gitea Authors. All rights reserved.
2
+
// SPDX-License-Identifier: MIT
3
+
4
+
package oauth2
5
+
6
+
import (
7
+
"context"
8
+
"time"
9
+
10
+
"code.gitea.io/gitea/models/auth"
11
+
"code.gitea.io/gitea/models/db"
12
+
user_model "code.gitea.io/gitea/models/user"
13
+
"code.gitea.io/gitea/modules/log"
14
+
15
+
"github.com/markbates/goth"
16
+
"golang.org/x/oauth2"
17
+
)
18
+
19
+
// Sync causes this OAuth2 source to synchronize its users with the db.
20
+
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
21
+
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID)
22
+
23
+
if !updateExisting {
24
+
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name)
25
+
return nil
26
+
}
27
+
28
+
provider, err := createProvider(source.authSource.Name, source)
29
+
if err != nil {
30
+
return err
31
+
}
32
+
33
+
if !provider.RefreshTokenAvailable() {
34
+
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name)
35
+
return nil
36
+
}
37
+
38
+
opts := user_model.FindExternalUserOptions{
39
+
HasRefreshToken: true,
40
+
Expired: true,
41
+
LoginSourceID: source.authSource.ID,
42
+
}
43
+
44
+
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
45
+
return source.refresh(ctx, provider, u)
46
+
})
47
+
}
48
+
49
+
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error {
50
+
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt)
51
+
52
+
shouldDisable := false
53
+
54
+
token, err := provider.RefreshToken(u.RefreshToken)
55
+
if err != nil {
56
+
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" {
57
+
// this signals that the token is not valid and the user should be disabled
58
+
shouldDisable = true
59
+
} else {
60
+
return err
61
+
}
62
+
}
63
+
64
+
user := &user_model.User{
65
+
LoginName: u.ExternalID,
66
+
LoginType: auth.OAuth2,
67
+
LoginSource: u.LoginSourceID,
68
+
}
69
+
70
+
hasUser, err := user_model.GetUser(ctx, user)
71
+
if err != nil {
72
+
return err
73
+
}
74
+
75
+
// If the grant is no longer valid, disable the user and
76
+
// delete local tokens. If the OAuth2 provider still
77
+
// recognizes them as a valid user, they will be able to login
78
+
// via their provider and reactivate their account.
79
+
if shouldDisable {
80
+
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID)
81
+
82
+
return db.WithTx(ctx, func(ctx context.Context) error {
83
+
if hasUser {
84
+
user.IsActive = false
85
+
err := user_model.UpdateUserCols(ctx, user, "is_active")
86
+
if err != nil {
87
+
return err
88
+
}
89
+
}
90
+
91
+
// Delete stored tokens, since they are invalid. This
92
+
// also provents us from checking this in subsequent runs.
93
+
u.AccessToken = ""
94
+
u.RefreshToken = ""
95
+
u.ExpiresAt = time.Time{}
96
+
97
+
return user_model.UpdateExternalUserByExternalID(ctx, u)
98
+
})
99
+
}
100
+
101
+
// Otherwise, update the tokens
102
+
u.AccessToken = token.AccessToken
103
+
u.ExpiresAt = token.Expiry
104
+
105
+
// Some providers only update access tokens provide a new
106
+
// refresh token, so avoid updating it if it's empty
107
+
if token.RefreshToken != "" {
108
+
u.RefreshToken = token.RefreshToken
109
+
}
110
+
111
+
err = user_model.UpdateExternalUserByExternalID(ctx, u)
112
+
113
+
return err
114
+
}
+100
services/auth/source/oauth2/source_sync_test.go
+100
services/auth/source/oauth2/source_sync_test.go
···
1
+
// Copyright 2024 The Gitea Authors. All rights reserved.
2
+
// SPDX-License-Identifier: MIT
3
+
4
+
package oauth2
5
+
6
+
import (
7
+
"context"
8
+
"testing"
9
+
10
+
"code.gitea.io/gitea/models/auth"
11
+
"code.gitea.io/gitea/models/unittest"
12
+
user_model "code.gitea.io/gitea/models/user"
13
+
14
+
"github.com/stretchr/testify/assert"
15
+
)
16
+
17
+
func TestSource(t *testing.T) {
18
+
assert.NoError(t, unittest.PrepareTestDatabase())
19
+
20
+
source := &Source{
21
+
Provider: "fake",
22
+
authSource: &auth.Source{
23
+
ID: 12,
24
+
Type: auth.OAuth2,
25
+
Name: "fake",
26
+
IsActive: true,
27
+
IsSyncEnabled: true,
28
+
},
29
+
}
30
+
31
+
user := &user_model.User{
32
+
LoginName: "external",
33
+
LoginType: auth.OAuth2,
34
+
LoginSource: source.authSource.ID,
35
+
Name: "test",
36
+
Email: "external@example.com",
37
+
}
38
+
39
+
err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{})
40
+
assert.NoError(t, err)
41
+
42
+
e := &user_model.ExternalLoginUser{
43
+
ExternalID: "external",
44
+
UserID: user.ID,
45
+
LoginSourceID: user.LoginSource,
46
+
RefreshToken: "valid",
47
+
}
48
+
err = user_model.LinkExternalToUser(context.Background(), user, e)
49
+
assert.NoError(t, err)
50
+
51
+
provider, err := createProvider(source.authSource.Name, source)
52
+
assert.NoError(t, err)
53
+
54
+
t.Run("refresh", func(t *testing.T) {
55
+
t.Run("valid", func(t *testing.T) {
56
+
err := source.refresh(context.Background(), provider, e)
57
+
assert.NoError(t, err)
58
+
59
+
e := &user_model.ExternalLoginUser{
60
+
ExternalID: e.ExternalID,
61
+
LoginSourceID: e.LoginSourceID,
62
+
}
63
+
64
+
ok, err := user_model.GetExternalLogin(context.Background(), e)
65
+
assert.NoError(t, err)
66
+
assert.True(t, ok)
67
+
assert.Equal(t, e.RefreshToken, "refresh")
68
+
assert.Equal(t, e.AccessToken, "token")
69
+
70
+
u, err := user_model.GetUserByID(context.Background(), user.ID)
71
+
assert.NoError(t, err)
72
+
assert.True(t, u.IsActive)
73
+
})
74
+
75
+
t.Run("expired", func(t *testing.T) {
76
+
err := source.refresh(context.Background(), provider, &user_model.ExternalLoginUser{
77
+
ExternalID: "external",
78
+
UserID: user.ID,
79
+
LoginSourceID: user.LoginSource,
80
+
RefreshToken: "expired",
81
+
})
82
+
assert.NoError(t, err)
83
+
84
+
e := &user_model.ExternalLoginUser{
85
+
ExternalID: e.ExternalID,
86
+
LoginSourceID: e.LoginSourceID,
87
+
}
88
+
89
+
ok, err := user_model.GetExternalLogin(context.Background(), e)
90
+
assert.NoError(t, err)
91
+
assert.True(t, ok)
92
+
assert.Equal(t, e.RefreshToken, "")
93
+
assert.Equal(t, e.AccessToken, "")
94
+
95
+
u, err := user_model.GetUserByID(context.Background(), user.ID)
96
+
assert.NoError(t, err)
97
+
assert.False(t, u.IsActive)
98
+
})
99
+
})
100
+
}
+3
-3
services/externalaccount/user.go
+3
-3
services/externalaccount/user.go
···
71
71
return nil
72
72
}
73
73
74
-
// UpdateExternalUser updates external user's information
75
-
func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
74
+
// EnsureLinkExternalToUser link the gothUser to the user
75
+
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
76
76
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
77
77
if err != nil {
78
78
return err
79
79
}
80
80
81
-
return user_model.UpdateExternalUserByExternalID(ctx, externalLoginUser)
81
+
return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
82
82
}
83
83
84
84
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
+17
-1
services/mailer/mail.go
+17
-1
services/mailer/mail.go
···
313
313
for _, recipient := range recipients {
314
314
msg := NewMessageFrom(
315
315
recipient.Email,
316
-
ctx.Doer.GetCompleteName(),
316
+
fromDisplayName(ctx.Doer),
317
317
setting.MailService.FromEmail,
318
318
subject,
319
319
mailBody.String(),
···
545
545
}
546
546
return typeName, name, template
547
547
}
548
+
549
+
func fromDisplayName(u *user_model.User) string {
550
+
if setting.MailService.FromDisplayNameFormatTemplate != nil {
551
+
var ctx bytes.Buffer
552
+
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
553
+
"DisplayName": u.DisplayName(),
554
+
"AppName": setting.AppName,
555
+
"Domain": setting.Domain,
556
+
})
557
+
if err == nil {
558
+
return mime.QEncoding.Encode("utf-8", ctx.String())
559
+
}
560
+
log.Error("fromDisplayName: %w", err)
561
+
}
562
+
return u.GetCompleteName()
563
+
}
+1
-1
services/mailer/mail_release.go
+1
-1
services/mailer/mail_release.go
···
85
85
}
86
86
87
87
msgs := make([]*Message, 0, len(tos))
88
-
publisherName := rel.Publisher.DisplayName()
88
+
publisherName := fromDisplayName(rel.Publisher)
89
89
msgID := createMessageIDForRelease(rel)
90
90
for _, to := range tos {
91
91
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())
+1
-1
services/mailer/mail_repo.go
+1
-1
services/mailer/mail_repo.go
···
79
79
}
80
80
81
81
for _, to := range emailTos {
82
-
msg := NewMessage(to.EmailTo(), subject, content.String())
82
+
msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String())
83
83
msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID)
84
84
85
85
SendAsync(msg)
+48
services/mailer/mail_test.go
+48
services/mailer/mail_test.go
···
489
489
})
490
490
}
491
491
}
492
+
493
+
func TestFromDisplayName(t *testing.T) {
494
+
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
495
+
assert.NoError(t, err)
496
+
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
497
+
defer func() { setting.MailService = nil }()
498
+
499
+
tests := []struct {
500
+
userDisplayName string
501
+
fromDisplayName string
502
+
}{{
503
+
userDisplayName: "test",
504
+
fromDisplayName: "test",
505
+
}, {
506
+
userDisplayName: "Hi Its <Mee>",
507
+
fromDisplayName: "Hi Its <Mee>",
508
+
}, {
509
+
userDisplayName: "Æsir",
510
+
fromDisplayName: "=?utf-8?q?=C3=86sir?=",
511
+
}, {
512
+
userDisplayName: "new😀user",
513
+
fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
514
+
}}
515
+
516
+
for _, tc := range tests {
517
+
t.Run(tc.userDisplayName, func(t *testing.T) {
518
+
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
519
+
got := fromDisplayName(user)
520
+
assert.EqualValues(t, tc.fromDisplayName, got)
521
+
})
522
+
}
523
+
524
+
t.Run("template with all available vars", func(t *testing.T) {
525
+
template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
526
+
assert.NoError(t, err)
527
+
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
528
+
oldAppName := setting.AppName
529
+
setting.AppName = "Code IT"
530
+
oldDomain := setting.Domain
531
+
setting.Domain = "code.it"
532
+
defer func() {
533
+
setting.AppName = oldAppName
534
+
setting.Domain = oldDomain
535
+
}()
536
+
537
+
assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
538
+
})
539
+
}
+1
-1
templates/admin/auth/edit.tmpl
+1
-1
templates/admin/auth/edit.tmpl
···
416
416
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
417
417
</div>
418
418
{{end}}
419
-
{{if .Source.IsLDAP}}
419
+
{{if (or .Source.IsLDAP .Source.IsOAuth2)}}
420
420
<div class="inline field">
421
421
<div class="ui checkbox">
422
422
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
+1
-1
templates/admin/auth/new.tmpl
+1
-1
templates/admin/auth/new.tmpl
···
59
59
<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
60
60
</div>
61
61
</div>
62
-
<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
62
+
<div class="oauth2 ldap inline field {{if not (or (eq .type 2) (eq .type 6))}}tw-hidden{{end}}">
63
63
<div class="ui checkbox">
64
64
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
65
65
<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>
+2
-1
tests/integration/auth_ldap_test.go
+2
-1
tests/integration/auth_ldap_test.go
···
244
244
}
245
245
defer tests.PrepareTestEnv(t)()
246
246
addAuthSourceLDAP(t, "", "", "", "")
247
-
auth.SyncExternalUsers(context.Background(), true)
247
+
err := auth.SyncExternalUsers(context.Background(), true)
248
+
assert.NoError(t, err)
248
249
249
250
// Check if users exists
250
251
for _, gitLDAPUser := range gitLDAPUsers {
-1
web_src/css/org.css
-1
web_src/css/org.css