loading up the forgejo repo on tangled to test page performance

Merge pull request '[gitea] week 2024-30 cherry pick (gitea/main -> forgejo)' (#4607) from algernon/wcp/2024-30 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4607
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

Changed files
+544 -66
custom
models
modules
issue
setting
release-notes
routers
web
services
templates
admin
tests
integration
web_src
+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
··· 216 216 return ErrSourceAlreadyExist{source.Name} 217 217 } 218 218 // Synchronization is only available with LDAP for now 219 - if !source.IsLDAP() { 219 + if !source.IsLDAP() && !source.IsOAuth2() { 220 220 source.IsSyncEnabled = false 221 221 } 222 222
+5 -1
models/migrations/v1_21/v279.go
··· 12 12 UserID int64 `xorm:"INDEX"` 13 13 } 14 14 15 - return x.Sync(new(Action)) 15 + _, err := x.SyncWithOptions(xorm.SyncOptions{ 16 + IgnoreDropIndices: true, 17 + IgnoreConstrains: true, 18 + }, new(Action)) 19 + return err 16 20 }
+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
··· 14 14 PreviousDuration time.Duration 15 15 } 16 16 17 - return x.Sync(&ActionRun{}) 17 + _, err := x.SyncWithOptions(xorm.SyncOptions{ 18 + IgnoreIndices: true, 19 + IgnoreConstrains: true, 20 + }, &ActionRun{}) 21 + return err 18 22 }
+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
··· 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
··· 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
··· 10 10 CommentID int64 `xorm:"INDEX"` 11 11 } 12 12 13 - return x.Sync(&Attachment{}) 13 + _, err := x.SyncWithOptions(xorm.SyncOptions{ 14 + IgnoreDropIndices: true, 15 + IgnoreConstrains: true, 16 + }, &Attachment{}) 17 + return err 14 18 }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 36 36 return json.UnmarshalHandleDoubleEncode(bs, &source) 37 37 } 38 38 39 - // ToDB exports an SMTPConfig to a serialized format. 39 + // ToDB exports an OAuth2Config to a serialized format. 40 40 func (source *Source) ToDB() ([]byte, error) { 41 41 return json.Marshal(source) 42 42 }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 96 96 .page-content.organization #org-info { 97 97 overflow-wrap: anywhere; 98 98 flex: 1; 99 - word-break: break-all; 100 99 } 101 100 102 101 .page-content.organization #org-info .ui.header {
+1 -1
web_src/css/repo.css
··· 2635 2635 .sidebar-item-link { 2636 2636 display: inline-flex; 2637 2637 align-items: center; 2638 - word-break: break-all; 2638 + overflow-wrap: anywhere; 2639 2639 } 2640 2640 2641 2641 .diff-file-header {