mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-21 08:31:27 -05:00
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:
commit
ded237ee77
31 changed files with 544 additions and 66 deletions
|
@ -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 =
|
||||||
;;
|
;;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = '')")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,12 +160,34 @@ 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
|
||||||
OrderBy string
|
LoginSourceID int64
|
||||||
|
HasRefreshToken bool
|
||||||
|
Expired bool
|
||||||
|
OrderBy string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
|
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, ", "))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_, _ = fmt.Fprint(builder, blankPlaceholder)
|
_, _ = fmt.Fprint(builder, blankPlaceholder)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
3
release-notes/4607.md
Normal 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.
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
14
services/auth/source/oauth2/main_test.go
Normal file
14
services/auth/source/oauth2/main_test.go
Normal 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{})
|
||||||
|
}
|
62
services/auth/source/oauth2/providers_test.go
Normal file
62
services/auth/source/oauth2/providers_test.go
Normal 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{}
|
||||||
|
}))
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
114
services/auth/source/oauth2/source_sync.go
Normal file
114
services/auth/source/oauth2/source_sync.go
Normal 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
|
||||||
|
}
|
100
services/auth/source/oauth2/source_sync_test.go
Normal file
100
services/auth/source/oauth2/source_sync_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue