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>
This commit is contained in:
Earl Warren 2024-07-23 16:01:28 +00:00
commit ded237ee77
31 changed files with 544 additions and 66 deletions

View file

@ -1727,6 +1727,10 @@ LEVEL = Info
;; 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. ;; 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.
;ENVELOPE_FROM = ;ENVELOPE_FROM =
;; ;;
;; 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>`,
;; set it to `{{ .DisplayName }} (by {{ .AppName }})`. Available Variables: `.DisplayName`, `.AppName` and `.Domain`.
;FROM_DISPLAY_NAME_FORMAT = {{ .DisplayName }}
;;
;; Mailer user name and password, if required by provider. ;; Mailer user name and password, if required by provider.
;USER = ;USER =
;; ;;

View file

@ -216,7 +216,7 @@ func CreateSource(ctx context.Context, source *Source) error {
return ErrSourceAlreadyExist{source.Name} return ErrSourceAlreadyExist{source.Name}
} }
// Synchronization is only available with LDAP for now // Synchronization is only available with LDAP for now
if !source.IsLDAP() { if !source.IsLDAP() && !source.IsOAuth2() {
source.IsSyncEnabled = false source.IsSyncEnabled = false
} }

View file

@ -12,5 +12,9 @@ func AddIndexToActionUserID(x *xorm.Engine) error {
UserID int64 `xorm:"INDEX"` UserID int64 `xorm:"INDEX"`
} }
return x.Sync(new(Action)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(Action))
return err
} }

View file

@ -10,5 +10,9 @@ func AddIgnoreStaleApprovalsColumnToProtectedBranchTable(x *xorm.Engine) error {
type ProtectedBranch struct { type ProtectedBranch struct {
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
} }
return x.Sync(new(ProtectedBranch)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(ProtectedBranch))
return err
} }

View file

@ -14,5 +14,9 @@ func AddPreviousDurationToActionRun(x *xorm.Engine) error {
PreviousDuration time.Duration PreviousDuration time.Duration
} }
return x.Sync(&ActionRun{}) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &ActionRun{})
return err
} }

View file

@ -54,7 +54,10 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
} }
if err := x.Sync(new(Repository)); err != nil { if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(Repository)); err != nil {
return err return err
} }

View file

@ -10,7 +10,10 @@ func AddDefaultWikiBranch(x *xorm.Engine) error {
ID int64 ID int64
DefaultWikiBranch string DefaultWikiBranch string
} }
if err := x.Sync(&Repository{}); err != nil { if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &Repository{}); err != nil {
return err return err
} }
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')") _, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")

View file

@ -35,5 +35,12 @@ type HookTask struct {
func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error { func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
// create missing column // create missing column
return x.Sync(new(HookTask)) if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(HookTask)); err != nil {
return err
}
_, err := x.Exec("UPDATE hook_task SET payload_version = 1 WHERE payload_version IS NULL")
return err
} }

View file

@ -10,5 +10,9 @@ func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
CommentID int64 `xorm:"INDEX"` CommentID int64 `xorm:"INDEX"`
} }
return x.Sync(&Attachment{}) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, &Attachment{})
return err
} }

View file

@ -160,11 +160,33 @@ func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLogin
return err return err
} }
// EnsureLinkExternalToUser link the external user to the user
func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error {
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
"external_id": external.ExternalID,
"login_source_id": external.LoginSourceID,
})
if err != nil {
return err
}
if has {
_, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external)
return err
}
_, err = db.GetEngine(ctx).Insert(external)
return err
}
// FindExternalUserOptions represents an options to find external users // FindExternalUserOptions represents an options to find external users
type FindExternalUserOptions struct { type FindExternalUserOptions struct {
db.ListOptions db.ListOptions
Provider string Provider string
UserID int64 UserID int64
LoginSourceID int64
HasRefreshToken bool
Expired bool
OrderBy string OrderBy string
} }
@ -176,9 +198,22 @@ func (opts FindExternalUserOptions) ToConds() builder.Cond {
if opts.UserID > 0 { if opts.UserID > 0 {
cond = cond.And(builder.Eq{"user_id": opts.UserID}) cond = cond.And(builder.Eq{"user_id": opts.UserID})
} }
if opts.Expired {
cond = cond.And(builder.Lt{"expires_at": time.Now()})
}
if opts.HasRefreshToken {
cond = cond.And(builder.Neq{"refresh_token": ""})
}
if opts.LoginSourceID != 0 {
cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
}
return cond return cond
} }
func (opts FindExternalUserOptions) ToOrders() string { func (opts FindExternalUserOptions) ToOrders() string {
return opts.OrderBy return opts.OrderBy
} }
func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error {
return db.Iterate(ctx, opts.ToConds(), f)
}

View file

@ -88,6 +88,9 @@ func validateYaml(template *api.IssueTemplate) error {
if err := validateBoolItem(position, field.Attributes, "multiple"); err != nil { if err := validateBoolItem(position, field.Attributes, "multiple"); err != nil {
return err return err
} }
if err := validateBoolItem(position, field.Attributes, "list"); err != nil {
return err
}
if err := validateOptions(field, idx); err != nil { if err := validateOptions(field, idx); err != nil {
return err return err
} }
@ -340,7 +343,13 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
} }
} }
if len(checkeds) > 0 { if len(checkeds) > 0 {
if list, ok := f.Attributes["list"].(bool); ok && list {
for _, check := range checkeds {
_, _ = fmt.Fprintf(builder, "- %s\n", check)
}
} else {
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", ")) _, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
}
} else { } else {
_, _ = fmt.Fprint(builder, blankPlaceholder) _, _ = fmt.Fprint(builder, blankPlaceholder)
} }

View file

@ -216,6 +216,20 @@ body:
`, `,
wantErr: "body[0](dropdown): 'multiple' should be a bool", wantErr: "body[0](dropdown): 'multiple' should be a bool",
}, },
{
name: "dropdown invalid list",
content: `
name: "test"
about: "this is about"
body:
- type: "dropdown"
id: "1"
attributes:
label: "a"
list: "on"
`,
wantErr: "body[0](dropdown): 'list' should be a bool",
},
{ {
name: "checkboxes invalid description", name: "checkboxes invalid description",
content: ` content: `
@ -807,7 +821,7 @@ body:
- type: dropdown - type: dropdown
id: id5 id: id5
attributes: attributes:
label: Label of dropdown label: Label of dropdown (one line)
description: Description of dropdown description: Description of dropdown
multiple: true multiple: true
options: options:
@ -816,8 +830,21 @@ body:
- Option 3 of dropdown - Option 3 of dropdown
validations: validations:
required: true required: true
- type: checkboxes - type: dropdown
id: id6 id: id6
attributes:
label: Label of dropdown (list)
description: Description of dropdown
multiple: true
list: true
options:
- Option 1 of dropdown
- Option 2 of dropdown
- Option 3 of dropdown
validations:
required: true
- type: checkboxes
id: id7
attributes: attributes:
label: Label of checkboxes label: Label of checkboxes
description: Description of checkboxes description: Description of checkboxes
@ -836,8 +863,9 @@ body:
"form-field-id3": {"Value of id3"}, "form-field-id3": {"Value of id3"},
"form-field-id4": {"Value of id4"}, "form-field-id4": {"Value of id4"},
"form-field-id5": {"0,1"}, "form-field-id5": {"0,1"},
"form-field-id6-0": {"on"}, "form-field-id6": {"1,2"},
"form-field-id6-2": {"on"}, "form-field-id7-0": {"on"},
"form-field-id7-2": {"on"},
}, },
}, },
@ -849,10 +877,15 @@ body:
Value of id4 Value of id4
### Label of dropdown ### Label of dropdown (one line)
Option 1 of dropdown, Option 2 of dropdown Option 1 of dropdown, Option 2 of dropdown
### Label of dropdown (list)
- Option 2 of dropdown
- Option 3 of dropdown
### Label of checkboxes ### Label of checkboxes
- [x] Option 1 of checkboxes - [x] Option 1 of checkboxes

View file

@ -8,6 +8,7 @@ import (
"net" "net"
"net/mail" "net/mail"
"strings" "strings"
"text/template"
"time" "time"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -46,6 +47,10 @@ type Mailer struct {
SendmailArgs []string `ini:"-"` SendmailArgs []string `ini:"-"`
SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"` SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"` SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
// Customization
FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
FromDisplayNameFormatTemplate *template.Template `ini:"-"`
} }
// MailService the global mailer // MailService the global mailer
@ -234,6 +239,16 @@ func loadMailerFrom(rootCfg ConfigProvider) {
log.Error("no mailer.FROM provided, email system may not work.") log.Error("no mailer.FROM provided, email system may not work.")
} }
MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
if MailService.FromDisplayNameFormat != "" {
template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
if err != nil {
log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
} else {
MailService.FromDisplayNameFormatTemplate = template
}
}
switch MailService.EnvelopeFrom { switch MailService.EnvelopeFrom {
case "": case "":
MailService.OverrideEnvelopeFrom = false MailService.OverrideEnvelopeFrom = false

3
release-notes/4607.md Normal file
View file

@ -0,0 +1,3 @@
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/21fdd28f084e7f1aef309c9ebd7599ffa6986453) allow synchronizing user status from OAuth2 login providers.
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/004cc6dc0ab7cc9c324ccb4ecd420c6aeeb20500) add option to change mail from user display name.
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/d0227c236aa195bd03990210f968b8e52eb20b79) issue Templates: add option to have dropdown printed list.

View file

@ -619,10 +619,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
notify_service.NewUserSignUp(ctx, u) notify_service.NewUserSignUp(ctx, u)
// update external user information // update external user information
if gothUser != nil { if gothUser != nil {
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) { log.Error("EnsureLinkExternalToUser failed: %v", err)
log.Error("UpdateExternalUser failed: %v", err)
}
} }
} }

View file

@ -1154,9 +1154,39 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
groups := getClaimedGroups(oauth2Source, &gothUser) groups := getClaimedGroups(oauth2Source, &gothUser)
opts := &user_service.UpdateOptions{}
// Reactivate user if they are deactivated
if !u.IsActive {
opts.IsActive = optional.Some(true)
}
// Update GroupClaims
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
ctx.ServerError("EnsureLinkExternalToUser", err)
return
}
// If this user is enrolled in 2FA and this source doesn't override it, // If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA { if !needs2FA {
// Register last login
opts.SetLastLogin = true
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := updateSession(ctx, nil, map[string]any{ if err := updateSession(ctx, nil, map[string]any{
"uid": u.ID, "uid": u.ID,
}); err != nil { }); err != nil {
@ -1167,29 +1197,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// Clear whatever CSRF cookie has right now, force to generate a new one // Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx) ctx.Csrf.DeleteCookie(ctx)
opts := &user_service.UpdateOptions{
SetLastLogin: true,
}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
// update external user information
if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) {
log.Error("UpdateExternalUser failed: %v", err)
}
}
if err := resetLocale(ctx, u); err != nil { if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err) ctx.ServerError("resetLocale", err)
return return
@ -1205,22 +1212,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return return
} }
opts := &user_service.UpdateOptions{} if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if err := user_service.UpdateUser(ctx, u, opts); err != nil { if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
return return
} }
} }
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := updateSession(ctx, nil, map[string]any{ if err := updateSession(ctx, nil, map[string]any{
// User needs to use 2FA, save data and redirect to 2FA page. // User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID, "twofaUid": u.ID,

View file

@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"testing"
"code.gitea.io/gitea/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{})
}

View file

@ -0,0 +1,62 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
type fakeProvider struct{}
func (p *fakeProvider) Name() string {
return "fake"
}
func (p *fakeProvider) SetName(name string) {}
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) {
return nil, nil
}
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) {
return nil, nil
}
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) {
return goth.User{}, nil
}
func (p *fakeProvider) Debug(bool) {
}
func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
switch refreshToken {
case "expired":
return nil, &oauth2.RetrieveError{
ErrorCode: "invalid_grant",
}
default:
return &oauth2.Token{
AccessToken: "token",
TokenType: "Bearer",
RefreshToken: "refresh",
Expiry: time.Now().Add(time.Hour),
}, nil
}
}
func (p *fakeProvider) RefreshTokenAvailable() bool {
return true
}
func init() {
RegisterGothProvider(
NewSimpleProvider("fake", "Fake", []string{"account"},
func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider {
return &fakeProvider{}
}))
}

View file

@ -36,7 +36,7 @@ func (source *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &source) return json.UnmarshalHandleDoubleEncode(bs, &source)
} }
// ToDB exports an SMTPConfig to a serialized format. // ToDB exports an OAuth2Config to a serialized format.
func (source *Source) ToDB() ([]byte, error) { func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source) return json.Marshal(source)
} }

View file

@ -0,0 +1,114 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"context"
"time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
// Sync causes this OAuth2 source to synchronize its users with the db.
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID)
if !updateExisting {
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name)
return nil
}
provider, err := createProvider(source.authSource.Name, source)
if err != nil {
return err
}
if !provider.RefreshTokenAvailable() {
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name)
return nil
}
opts := user_model.FindExternalUserOptions{
HasRefreshToken: true,
Expired: true,
LoginSourceID: source.authSource.ID,
}
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
return source.refresh(ctx, provider, u)
})
}
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error {
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt)
shouldDisable := false
token, err := provider.RefreshToken(u.RefreshToken)
if err != nil {
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" {
// this signals that the token is not valid and the user should be disabled
shouldDisable = true
} else {
return err
}
}
user := &user_model.User{
LoginName: u.ExternalID,
LoginType: auth.OAuth2,
LoginSource: u.LoginSourceID,
}
hasUser, err := user_model.GetUser(ctx, user)
if err != nil {
return err
}
// If the grant is no longer valid, disable the user and
// delete local tokens. If the OAuth2 provider still
// recognizes them as a valid user, they will be able to login
// via their provider and reactivate their account.
if shouldDisable {
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID)
return db.WithTx(ctx, func(ctx context.Context) error {
if hasUser {
user.IsActive = false
err := user_model.UpdateUserCols(ctx, user, "is_active")
if err != nil {
return err
}
}
// Delete stored tokens, since they are invalid. This
// also provents us from checking this in subsequent runs.
u.AccessToken = ""
u.RefreshToken = ""
u.ExpiresAt = time.Time{}
return user_model.UpdateExternalUserByExternalID(ctx, u)
})
}
// Otherwise, update the tokens
u.AccessToken = token.AccessToken
u.ExpiresAt = token.Expiry
// Some providers only update access tokens provide a new
// refresh token, so avoid updating it if it's empty
if token.RefreshToken != "" {
u.RefreshToken = token.RefreshToken
}
err = user_model.UpdateExternalUserByExternalID(ctx, u)
return err
}

View file

@ -0,0 +1,100 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"context"
"testing"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestSource(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
source := &Source{
Provider: "fake",
authSource: &auth.Source{
ID: 12,
Type: auth.OAuth2,
Name: "fake",
IsActive: true,
IsSyncEnabled: true,
},
}
user := &user_model.User{
LoginName: "external",
LoginType: auth.OAuth2,
LoginSource: source.authSource.ID,
Name: "test",
Email: "external@example.com",
}
err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{})
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: "external",
UserID: user.ID,
LoginSourceID: user.LoginSource,
RefreshToken: "valid",
}
err = user_model.LinkExternalToUser(context.Background(), user, e)
assert.NoError(t, err)
provider, err := createProvider(source.authSource.Name, source)
assert.NoError(t, err)
t.Run("refresh", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
err := source.refresh(context.Background(), provider, e)
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: e.ExternalID,
LoginSourceID: e.LoginSourceID,
}
ok, err := user_model.GetExternalLogin(context.Background(), e)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, e.RefreshToken, "refresh")
assert.Equal(t, e.AccessToken, "token")
u, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.True(t, u.IsActive)
})
t.Run("expired", func(t *testing.T) {
err := source.refresh(context.Background(), provider, &user_model.ExternalLoginUser{
ExternalID: "external",
UserID: user.ID,
LoginSourceID: user.LoginSource,
RefreshToken: "expired",
})
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: e.ExternalID,
LoginSourceID: e.LoginSourceID,
}
ok, err := user_model.GetExternalLogin(context.Background(), e)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, e.RefreshToken, "")
assert.Equal(t, e.AccessToken, "")
u, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.False(t, u.IsActive)
})
})
}

View file

@ -71,14 +71,14 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
return nil return nil
} }
// UpdateExternalUser updates external user's information // EnsureLinkExternalToUser link the gothUser to the user
func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error { func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser) externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
if err != nil { if err != nil {
return err return err
} }
return user_model.UpdateExternalUserByExternalID(ctx, externalLoginUser) return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
} }
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID

View file

@ -313,7 +313,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
for _, recipient := range recipients { for _, recipient := range recipients {
msg := NewMessageFrom( msg := NewMessageFrom(
recipient.Email, recipient.Email,
ctx.Doer.GetCompleteName(), fromDisplayName(ctx.Doer),
setting.MailService.FromEmail, setting.MailService.FromEmail,
subject, subject,
mailBody.String(), mailBody.String(),
@ -545,3 +545,19 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
} }
return typeName, name, template return typeName, name, template
} }
func fromDisplayName(u *user_model.User) string {
if setting.MailService.FromDisplayNameFormatTemplate != nil {
var ctx bytes.Buffer
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
"DisplayName": u.DisplayName(),
"AppName": setting.AppName,
"Domain": setting.Domain,
})
if err == nil {
return mime.QEncoding.Encode("utf-8", ctx.String())
}
log.Error("fromDisplayName: %w", err)
}
return u.GetCompleteName()
}

View file

@ -85,7 +85,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
} }
msgs := make([]*Message, 0, len(tos)) msgs := make([]*Message, 0, len(tos))
publisherName := rel.Publisher.DisplayName() publisherName := fromDisplayName(rel.Publisher)
msgID := createMessageIDForRelease(rel) msgID := createMessageIDForRelease(rel)
for _, to := range tos { for _, to := range tos {
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())

View file

@ -79,7 +79,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
} }
for _, to := range emailTos { for _, to := range emailTos {
msg := NewMessage(to.EmailTo(), subject, content.String()) msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID)
SendAsync(msg) SendAsync(msg)

View file

@ -489,3 +489,51 @@ func Test_createReference(t *testing.T) {
}) })
} }
} }
func TestFromDisplayName(t *testing.T) {
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
assert.NoError(t, err)
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
defer func() { setting.MailService = nil }()
tests := []struct {
userDisplayName string
fromDisplayName string
}{{
userDisplayName: "test",
fromDisplayName: "test",
}, {
userDisplayName: "Hi Its <Mee>",
fromDisplayName: "Hi Its <Mee>",
}, {
userDisplayName: "Æsir",
fromDisplayName: "=?utf-8?q?=C3=86sir?=",
}, {
userDisplayName: "new😀user",
fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
}}
for _, tc := range tests {
t.Run(tc.userDisplayName, func(t *testing.T) {
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
got := fromDisplayName(user)
assert.EqualValues(t, tc.fromDisplayName, got)
})
}
t.Run("template with all available vars", func(t *testing.T) {
template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
assert.NoError(t, err)
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
oldAppName := setting.AppName
setting.AppName = "Code IT"
oldDomain := setting.Domain
setting.Domain = "code.it"
defer func() {
setting.AppName = oldAppName
setting.Domain = oldDomain
}()
assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
})
}

View file

@ -416,7 +416,7 @@
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p> <p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
</div> </div>
{{end}} {{end}}
{{if .Source.IsLDAP}} {{if (or .Source.IsLDAP .Source.IsOAuth2)}}
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label> <label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>

View file

@ -59,7 +59,7 @@
<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}> <input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
</div> </div>
</div> </div>
<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}"> <div class="oauth2 ldap inline field {{if not (or (eq .type 2) (eq .type 6))}}tw-hidden{{end}}">
<div class="ui checkbox"> <div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label> <label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}> <input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>

View file

@ -244,7 +244,8 @@ func TestLDAPUserSync(t *testing.T) {
} }
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "", "", "") addAuthSourceLDAP(t, "", "", "", "")
auth.SyncExternalUsers(context.Background(), true) err := auth.SyncExternalUsers(context.Background(), true)
assert.NoError(t, err)
// Check if users exists // Check if users exists
for _, gitLDAPUser := range gitLDAPUsers { for _, gitLDAPUser := range gitLDAPUsers {

View file

@ -96,7 +96,6 @@
.page-content.organization #org-info { .page-content.organization #org-info {
overflow-wrap: anywhere; overflow-wrap: anywhere;
flex: 1; flex: 1;
word-break: break-all;
} }
.page-content.organization #org-info .ui.header { .page-content.organization #org-info .ui.header {

View file

@ -2635,7 +2635,7 @@ tbody.commit-list {
.sidebar-item-link { .sidebar-item-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
word-break: break-all; overflow-wrap: anywhere;
} }
.diff-file-header { .diff-file-header {