mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-01 05:01:26 +00:00
Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187)
Fix #880 Design: 1. A global setting `security.TWO_FACTOR_AUTH`. * To support org-level config, we need to introduce a better "owner setting" system first (in the future) 2. A user without 2FA can login and may explore, but can NOT read or write to any repositories via API/web. 3. Keep things as simple as possible. * This option only aggressively suggest users to enable their 2FA at the moment, it does NOT guarantee that users must have 2FA before all other operations, it should be good enough for real world use cases. * Some details and tests could be improved in the future since this change only adds a check and seems won't affect too much. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
4ed07244b9
commit
0148d03f21
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -210,8 +211,8 @@ func newAuthService() *authService {
|
||||
}
|
||||
}
|
||||
|
||||
// parseAuthSource assigns values on authSource according to command line flags.
|
||||
func parseAuthSource(c *cli.Context, authSource *auth.Source) {
|
||||
// parseAuthSourceLdap assigns values on authSource according to command line flags.
|
||||
func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
|
||||
if c.IsSet("name") {
|
||||
authSource.Name = c.String("name")
|
||||
}
|
||||
@ -227,6 +228,7 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) {
|
||||
if c.IsSet("disable-synchronize-users") {
|
||||
authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
|
||||
}
|
||||
authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||
}
|
||||
|
||||
// parseLdapConfig assigns values on config according to command line flags.
|
||||
@ -298,9 +300,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
|
||||
if c.IsSet("allow-deactivate-all") {
|
||||
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
|
||||
}
|
||||
if c.IsSet("skip-local-2fa") {
|
||||
config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
|
||||
}
|
||||
if c.IsSet("enable-groups") {
|
||||
config.GroupsEnabled = c.Bool("enable-groups")
|
||||
}
|
||||
@ -376,7 +375,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
|
||||
},
|
||||
}
|
||||
|
||||
parseAuthSource(c, authSource)
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -398,7 +397,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
parseAuthSource(c, authSource)
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -427,7 +426,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
|
||||
},
|
||||
}
|
||||
|
||||
parseAuthSource(c, authSource)
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -449,7 +448,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
parseAuthSource(c, authSource)
|
||||
parseAuthSourceLdap(c, authSource)
|
||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -156,7 +157,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
|
||||
CustomURLMapping: customURLMapping,
|
||||
IconURL: c.String("icon-url"),
|
||||
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
|
||||
Scopes: c.StringSlice("scopes"),
|
||||
RequiredClaimName: c.String("required-claim-name"),
|
||||
RequiredClaimValue: c.String("required-claim-value"),
|
||||
@ -185,10 +185,11 @@ func runAddOauth(c *cli.Context) error {
|
||||
}
|
||||
|
||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: c.String("name"),
|
||||
IsActive: true,
|
||||
Cfg: config,
|
||||
Type: auth_model.OAuth2,
|
||||
Name: c.String("name"),
|
||||
IsActive: true,
|
||||
Cfg: config,
|
||||
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
|
||||
})
|
||||
}
|
||||
|
||||
@ -294,6 +295,6 @@ func runUpdateOauth(c *cli.Context) error {
|
||||
|
||||
oAuth2Config.CustomURLMapping = customURLMapping
|
||||
source.Cfg = oAuth2Config
|
||||
|
||||
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||
return auth_model.UpdateSource(ctx, source)
|
||||
}
|
||||
|
@ -117,9 +117,6 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
|
||||
if c.IsSet("disable-helo") {
|
||||
conf.DisableHelo = c.Bool("disable-helo")
|
||||
}
|
||||
if c.IsSet("skip-local-2fa") {
|
||||
conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -156,10 +153,11 @@ func runAddSMTP(c *cli.Context) error {
|
||||
}
|
||||
|
||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||
Type: auth_model.SMTP,
|
||||
Name: c.String("name"),
|
||||
IsActive: active,
|
||||
Cfg: &smtpConfig,
|
||||
Type: auth_model.SMTP,
|
||||
Name: c.String("name"),
|
||||
IsActive: active,
|
||||
Cfg: &smtpConfig,
|
||||
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
|
||||
})
|
||||
}
|
||||
|
||||
@ -195,6 +193,6 @@ func runUpdateSMTP(c *cli.Context) error {
|
||||
}
|
||||
|
||||
source.Cfg = smtpConfig
|
||||
|
||||
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||
return auth_model.UpdateSource(ctx, source)
|
||||
}
|
||||
|
@ -524,6 +524,10 @@ INTERNAL_TOKEN =
|
||||
;;
|
||||
;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
|
||||
;; RECORD_USER_SIGNUP_METADATA = false
|
||||
;;
|
||||
;; Set the two-factor auth behavior.
|
||||
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
|
||||
;TWO_FACTOR_AUTH =
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -58,6 +58,15 @@ var Names = map[Type]string{
|
||||
// Config represents login config as far as the db is concerned
|
||||
type Config interface {
|
||||
convert.Conversion
|
||||
SetAuthSource(*Source)
|
||||
}
|
||||
|
||||
type ConfigBase struct {
|
||||
AuthSource *Source
|
||||
}
|
||||
|
||||
func (p *ConfigBase) SetAuthSource(s *Source) {
|
||||
p.AuthSource = s
|
||||
}
|
||||
|
||||
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
|
||||
@ -104,19 +113,15 @@ func RegisterTypeConfig(typ Type, exemplar Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// SourceSettable configurations can have their authSource set on them
|
||||
type SourceSettable interface {
|
||||
SetAuthSource(*Source)
|
||||
}
|
||||
|
||||
// Source represents an external way for authorizing users.
|
||||
type Source struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type Type
|
||||
Name string `xorm:"UNIQUE"`
|
||||
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Cfg convert.Conversion `xorm:"TEXT"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type Type
|
||||
Name string `xorm:"UNIQUE"`
|
||||
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
|
||||
Cfg Config `xorm:"TEXT"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) {
|
||||
return
|
||||
}
|
||||
source.Cfg = constructor()
|
||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
||||
settable.SetAuthSource(source)
|
||||
}
|
||||
source.Cfg.SetAuthSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool {
|
||||
return ok && skipVerifiable.IsSkipVerify()
|
||||
}
|
||||
|
||||
func (source *Source) TwoFactorShouldSkip() bool {
|
||||
return source.TwoFactorPolicy == "skip"
|
||||
}
|
||||
|
||||
// CreateSource inserts a AuthSource in the DB if not already
|
||||
// existing with the given name.
|
||||
func CreateSource(ctx context.Context, source *Source) error {
|
||||
@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
||||
settable.SetAuthSource(source)
|
||||
}
|
||||
source.Cfg.SetAuthSource(source)
|
||||
|
||||
registerableSource, ok := source.Cfg.(RegisterableSource)
|
||||
if !ok {
|
||||
@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
||||
settable.SetAuthSource(source)
|
||||
}
|
||||
source.Cfg.SetAuthSource(source)
|
||||
|
||||
registerableSource, ok := source.Cfg.(RegisterableSource)
|
||||
if !ok {
|
||||
|
@ -19,6 +19,8 @@ import (
|
||||
)
|
||||
|
||||
type TestSource struct {
|
||||
auth_model.ConfigBase
|
||||
|
||||
Provider string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
|
@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) {
|
||||
has, err := HasTwoFactorByUID(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if has {
|
||||
return true, nil
|
||||
}
|
||||
return HasWebAuthnRegistrationsByUID(ctx, id)
|
||||
}
|
||||
|
@ -381,6 +381,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
|
||||
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
|
||||
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
57
models/migrations/v1_24/v320.go
Normal file
57
models/migrations/v1_24/v320.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_24 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func MigrateSkipTwoFactor(x *xorm.Engine) error {
|
||||
type LoginSource struct {
|
||||
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(
|
||||
xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
},
|
||||
new(LoginSource),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type LoginSourceSimple struct {
|
||||
ID int64
|
||||
Cfg string
|
||||
}
|
||||
|
||||
var loginSources []LoginSourceSimple
|
||||
err = x.Table("login_source").Find(&loginSources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, source := range loginSources {
|
||||
if source.Cfg == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
err = json.Unmarshal([]byte(source.Cfg), &cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg["SkipLocalTwoFA"] == true {
|
||||
_, err = x.Exec("UPDATE login_source SET two_factor_policy = 'skip' WHERE id = ?", source.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -522,3 +522,7 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u
|
||||
|
||||
return perm.CanRead(unitType)
|
||||
}
|
||||
|
||||
func PermissionNoAccess() Permission {
|
||||
return Permission{AccessMode: perm_model.AccessModeNone}
|
||||
}
|
||||
|
11
modules/session/key.go
Normal file
11
modules/session/key.go
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
const (
|
||||
KeyUID = "uid"
|
||||
KeyUname = "uname"
|
||||
|
||||
KeyUserHasTwoFactorAuth = "userHasTwoFactorAuth"
|
||||
)
|
@ -39,6 +39,7 @@ var (
|
||||
CSRFCookieName = "_csrf"
|
||||
CSRFCookieHTTPOnly = true
|
||||
RecordUserSignupMetadata = false
|
||||
TwoFactorAuthEnforced = false
|
||||
)
|
||||
|
||||
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
|
||||
@ -142,6 +143,15 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
||||
|
||||
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
|
||||
switch twoFactorAuth {
|
||||
case "":
|
||||
case "enforced":
|
||||
TwoFactorAuthEnforced = true
|
||||
default:
|
||||
log.Fatal("Invalid two-factor auth option: %s", twoFactorAuth)
|
||||
}
|
||||
|
||||
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||
if InstallLock && InternalToken == "" {
|
||||
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
|
||||
|
@ -450,6 +450,7 @@ use_scratch_code = Use a scratch code
|
||||
twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code.
|
||||
twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your device, use your scratch code to sign in.
|
||||
twofa_scratch_token_incorrect = Your scratch code is incorrect.
|
||||
twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again.
|
||||
login_userpass = Sign In
|
||||
login_openid = OpenID
|
||||
oauth_signup_tab = Register New Account
|
||||
|
@ -64,6 +64,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -211,11 +212,20 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
}
|
||||
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
|
||||
} else {
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if needTwoFactor {
|
||||
ctx.Repo.Permission = access_model.PermissionNoAccess()
|
||||
} else {
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.HasAnyUnitAccess() {
|
||||
@ -225,6 +235,20 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool, error) {
|
||||
if !setting.TwoFactorAuthEnforced {
|
||||
return false, nil
|
||||
}
|
||||
if doer == nil {
|
||||
return false, nil
|
||||
}
|
||||
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !has, nil
|
||||
}
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
|
@ -28,8 +28,6 @@ import (
|
||||
"code.gitea.io/gitea/services/auth/source/sspi"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"xorm.io/xorm/convert"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -149,7 +147,6 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
||||
RestrictedFilter: form.RestrictedFilter,
|
||||
AllowDeactivateAll: form.AllowDeactivateAll,
|
||||
Enabled: true,
|
||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +160,6 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
||||
SkipVerify: form.SkipVerify,
|
||||
HeloHostname: form.HeloHostname,
|
||||
DisableHelo: form.DisableHelo,
|
||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +194,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||
Scopes: scopes,
|
||||
RequiredClaimName: form.Oauth2RequiredClaimName,
|
||||
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
||||
GroupClaimName: form.Oauth2GroupClaimName,
|
||||
RestrictedGroup: form.Oauth2RestrictedGroup,
|
||||
AdminGroup: form.Oauth2AdminGroup,
|
||||
@ -252,7 +247,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
hasTLS := false
|
||||
var config convert.Conversion
|
||||
var config auth.Config
|
||||
switch auth.Type(form.Type) {
|
||||
case auth.LDAP, auth.DLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
@ -262,9 +257,8 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
hasTLS = true
|
||||
case auth.PAM:
|
||||
config = &pam_service.Source{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
case auth.OAuth2:
|
||||
config = parseOAuth2Config(form)
|
||||
@ -302,11 +296,12 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if err := auth.CreateSource(ctx, &auth.Source{
|
||||
Type: auth.Type(form.Type),
|
||||
Name: form.Name,
|
||||
IsActive: form.IsActive,
|
||||
IsSyncEnabled: form.IsSyncEnabled,
|
||||
Cfg: config,
|
||||
Type: auth.Type(form.Type),
|
||||
Name: form.Name,
|
||||
IsActive: form.IsActive,
|
||||
IsSyncEnabled: form.IsSyncEnabled,
|
||||
TwoFactorPolicy: form.TwoFactorPolicy,
|
||||
Cfg: config,
|
||||
}); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
@ -384,7 +379,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var config convert.Conversion
|
||||
var config auth.Config
|
||||
switch auth.Type(form.Type) {
|
||||
case auth.LDAP, auth.DLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
@ -421,6 +416,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
source.IsActive = form.IsActive
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
source.TwoFactorPolicy = form.TwoFactorPolicy
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@ -87,6 +88,7 @@ func TwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||
handleSignIn(ctx, u, remember)
|
||||
return
|
||||
}
|
||||
|
@ -76,6 +76,10 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
|
||||
}
|
||||
|
||||
isSucceed = true
|
||||
|
||||
@ -87,9 +91,9 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
// Set session IDs
|
||||
"uid": u.ID,
|
||||
"uname": u.Name,
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("unable to updateSession: %w", err)
|
||||
}
|
||||
@ -239,9 +243,8 @@ func SignInPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Now handle 2FA:
|
||||
|
||||
// First of all if the source can skip local two fa we're done
|
||||
if skipper, ok := source.Cfg.(auth_service.LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
|
||||
if source.TwoFactorShouldSkip() {
|
||||
handleSignIn(ctx, u, form.Remember)
|
||||
return
|
||||
}
|
||||
@ -262,7 +265,7 @@ func SignInPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !hasTOTPtwofa && !hasWebAuthnTwofa {
|
||||
// No two factor auth configured we can sign in the user
|
||||
// No two-factor auth configured we can sign in the user
|
||||
handleSignIn(ctx, u, form.Remember)
|
||||
return
|
||||
}
|
||||
@ -311,8 +314,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
}
|
||||
|
||||
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, []string{
|
||||
// Delete the openid, 2fa and linkaccount data
|
||||
// Delete the openid, 2fa and link_account data
|
||||
"openid_verified_uri",
|
||||
"openid_signin_remember",
|
||||
"openid_determined_email",
|
||||
@ -321,8 +330,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
"twofaRemember",
|
||||
"linkAccount",
|
||||
}, map[string]any{
|
||||
"uid": u.ID,
|
||||
"uname": u.Name,
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
}); err != nil {
|
||||
ctx.ServerError("RegenerateSession", err)
|
||||
return setting.AppSubURL + "/"
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
@ -302,7 +303,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
|
||||
|
||||
needs2FA := false
|
||||
if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
|
||||
if !source.TwoFactorShouldSkip() {
|
||||
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
@ -352,10 +353,16 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
"uid": u.ID,
|
||||
"uname": u.Name,
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
}); err != nil {
|
||||
ctx.ServerError("updateSession", err)
|
||||
return
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -163,6 +164,7 @@ func EnrollTwoFactor(ctx *context.Context) {
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsSecurity"] = true
|
||||
ctx.Data["ShowTwoFactorRequiredMessage"] = false
|
||||
|
||||
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if t != nil {
|
||||
@ -194,6 +196,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsSecurity"] = true
|
||||
ctx.Data["ShowTwoFactorRequiredMessage"] = false
|
||||
|
||||
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if t != nil {
|
||||
@ -246,6 +249,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
newTwoFactorErr := auth.NewTwoFactor(ctx, t)
|
||||
if newTwoFactorErr == nil {
|
||||
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||
}
|
||||
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
|
||||
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
|
||||
if err := ctx.Session.Delete("twofaSecret"); err != nil {
|
||||
@ -261,10 +268,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||
log.Error("Unable to save changes to the session: %v", err)
|
||||
}
|
||||
|
||||
if err = auth.NewTwoFactor(ctx, t); err != nil {
|
||||
if newTwoFactorErr != nil {
|
||||
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
|
||||
// If there is a unique constraint fail we should just tolerate the error
|
||||
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
|
||||
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
wa "code.gitea.io/gitea/modules/auth/webauthn"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -120,7 +121,7 @@ func WebauthnRegisterPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
_ = ctx.Session.Delete("webauthnName")
|
||||
|
||||
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||
ctx.JSON(http.StatusCreated, cred)
|
||||
}
|
||||
|
||||
|
@ -142,14 +142,14 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
// Check if the user has webAuthn registration
|
||||
if !source.TwoFactorShouldSkip() {
|
||||
// Check if the user has WebAuthn registration
|
||||
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasWebAuthn {
|
||||
return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
|
||||
return nil, errors.New("basic authorization is not allowed while WebAuthn enrolled")
|
||||
}
|
||||
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
|
@ -35,11 +35,6 @@ type PasswordAuthenticator interface {
|
||||
Authenticate(ctx context.Context, user *user_model.User, login, password string) (*user_model.User, error)
|
||||
}
|
||||
|
||||
// LocalTwoFASkipper represents a source of authentication that can skip local 2fa
|
||||
type LocalTwoFASkipper interface {
|
||||
IsSkipLocalTwoFA() bool
|
||||
}
|
||||
|
||||
// SynchronizableSource represents a source that can synchronize users
|
||||
type SynchronizableSource interface {
|
||||
Sync(ctx context.Context, updateExisting bool) error
|
||||
|
@ -11,7 +11,9 @@ import (
|
||||
)
|
||||
|
||||
// Source is a password authentication service
|
||||
type Source struct{}
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
}
|
||||
|
||||
// FromDB fills up an OAuth2Config from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
|
@ -15,13 +15,11 @@ import (
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
auth.SynchronizableSource
|
||||
auth.LocalTwoFASkipper
|
||||
auth_model.SSHKeyProvider
|
||||
auth_model.Config
|
||||
auth_model.SkipVerifiable
|
||||
auth_model.HasTLSer
|
||||
auth_model.UseTLSer
|
||||
auth_model.SourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &ldap.Source{}
|
||||
|
@ -24,6 +24,8 @@ import (
|
||||
|
||||
// Source Basic LDAP authentication service
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
Name string // canonical name (ie. corporate.ad)
|
||||
Host string // LDAP host
|
||||
Port int // port number
|
||||
@ -54,9 +56,6 @@ type Source struct {
|
||||
GroupTeamMap string // Map LDAP groups to teams
|
||||
GroupTeamMapRemoval bool // Remove user from teams which are synchronized and user is not a member of the corresponding LDAP group
|
||||
UserUID string // User Attribute listed in Group
|
||||
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
|
||||
|
||||
authSource *auth.Source // reference to the authSource
|
||||
}
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
@ -109,11 +108,6 @@ func (source *Source) ProvidesSSHKeys() bool {
|
||||
return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
||||
}
|
||||
|
||||
// SetAuthSource sets the related AuthSource
|
||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
||||
source.authSource = authSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.LDAP, &Source{})
|
||||
auth.RegisterTypeConfig(auth.DLDAP, &Source{})
|
||||
|
@ -25,7 +25,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
if user != nil {
|
||||
loginName = user.LoginName
|
||||
}
|
||||
sr := source.SearchEntry(loginName, password, source.authSource.Type == auth.DLDAP)
|
||||
sr := source.SearchEntry(loginName, password, source.AuthSource.Type == auth.DLDAP)
|
||||
if sr == nil {
|
||||
// User not in LDAP, do nothing
|
||||
return nil, user_model.ErrUserNotExist{Name: loginName}
|
||||
@ -73,7 +73,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey) {
|
||||
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return user, err
|
||||
}
|
||||
@ -84,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.authSource.Type,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginType: source.AuthSource.Type,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
LoginName: userName,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
}
|
||||
@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
return user, err
|
||||
}
|
||||
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey) {
|
||||
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return user, err
|
||||
}
|
||||
@ -123,8 +123,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
|
||||
func (source *Source) IsSkipLocalTwoFA() bool {
|
||||
return source.SkipLocalTwoFA
|
||||
}
|
||||
|
@ -22,21 +22,21 @@ import (
|
||||
|
||||
// Sync causes this ldap source to synchronize its users with the db
|
||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name)
|
||||
log.Trace("Doing: SyncExternalUsers[%s]", source.AuthSource.Name)
|
||||
|
||||
isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
||||
var sshKeysNeedUpdate bool
|
||||
|
||||
// Find all users with this login type - FIXME: Should this be an iterator?
|
||||
users, err := user_model.GetUsersBySource(ctx, source.authSource)
|
||||
users, err := user_model.GetUsersBySource(ctx, source.AuthSource)
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers: %v", err)
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.authSource.Name)
|
||||
return db.ErrCancelledf("Before update of %s", source.authSource.Name)
|
||||
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.AuthSource.Name)
|
||||
return db.ErrCancelledf("Before update of %s", source.AuthSource.Name)
|
||||
default:
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
|
||||
sr, err := source.SearchEntries()
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.authSource.Name)
|
||||
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.AuthSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
for _, su := range sr {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
|
||||
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.AuthSource.Name)
|
||||
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
||||
if sshKeysNeedUpdate {
|
||||
err = asymkey_service.RewriteAllPublicKeys(ctx)
|
||||
@ -82,7 +82,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
log.Error("RewriteAllPublicKeys: %v", err)
|
||||
}
|
||||
}
|
||||
return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name)
|
||||
return db.ErrCancelledf("During update of %s before completed update of users", source.AuthSource.Name)
|
||||
default:
|
||||
}
|
||||
if su.Username == "" && su.Mail == "" {
|
||||
@ -111,14 +111,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
fullName := composeFullName(su.Name, su.Surname, su.Username)
|
||||
// If no existing user found, create one
|
||||
if usr == nil {
|
||||
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.authSource.Name, su.Username)
|
||||
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.AuthSource.Name, su.Username)
|
||||
|
||||
usr = &user_model.User{
|
||||
LowerName: su.LowerName,
|
||||
Name: su.Username,
|
||||
FullName: fullName,
|
||||
LoginType: source.authSource.Type,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginType: source.AuthSource.Type,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
LoginName: su.Username,
|
||||
Email: su.Mail,
|
||||
IsAdmin: su.IsAdmin,
|
||||
@ -130,12 +130,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
|
||||
err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
|
||||
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.AuthSource.Name, su.Username, err)
|
||||
}
|
||||
|
||||
if err == nil && isAttributeSSHPublicKeySet {
|
||||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.authSource.Name, usr.Name)
|
||||
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.authSource, su.SSHPublicKey) {
|
||||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.AuthSource.Name, usr.Name)
|
||||
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
}
|
||||
@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
}
|
||||
} else if updateExisting {
|
||||
// Synchronize SSH Public Key if that attribute is set
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.authSource, su.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
!strings.EqualFold(usr.Email, su.Mail) ||
|
||||
usr.FullName != fullName ||
|
||||
!usr.IsActive {
|
||||
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name)
|
||||
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.AuthSource.Name, usr.Name)
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.Some(fullName),
|
||||
@ -170,11 +170,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err)
|
||||
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.AuthSource.Name, usr.Name, err)
|
||||
}
|
||||
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, usr, su.Mail); err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error updating user %s primary email %s: %v", source.authSource.Name, usr.Name, su.Mail, err)
|
||||
log.Error("SyncExternalUsers[%s]: Error updating user %s primary email %s: %v", source.AuthSource.Name, usr.Name, su.Mail, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,8 +202,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.authSource.Name)
|
||||
return db.ErrCancelledf("During update of %s before delete users", source.authSource.Name)
|
||||
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.AuthSource.Name)
|
||||
return db.ErrCancelledf("During update of %s before delete users", source.AuthSource.Name)
|
||||
default:
|
||||
}
|
||||
|
||||
@ -214,13 +214,13 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
|
||||
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.AuthSource.Name, usr.Name)
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
IsActive: optional.Some(false),
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
|
||||
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.AuthSource.Name, usr.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
|
||||
type sourceInterface interface {
|
||||
auth_model.Config
|
||||
auth_model.SourceSettable
|
||||
auth_model.RegisterableSource
|
||||
auth.PasswordAuthenticator
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
// Source holds configuration for the OAuth2 login source.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
Provider string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
@ -25,10 +27,6 @@ type Source struct {
|
||||
GroupTeamMap string
|
||||
GroupTeamMapRemoval bool
|
||||
RestrictedGroup string
|
||||
SkipLocalTwoFA bool `json:",omitempty"`
|
||||
|
||||
// reference to the authSource
|
||||
authSource *auth.Source
|
||||
}
|
||||
|
||||
// FromDB fills up an OAuth2Config from serialized format.
|
||||
@ -41,11 +39,6 @@ func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// SetAuthSource sets the related AuthSource
|
||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
||||
source.authSource = authSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
// Callout redirects request/response pair to authenticate against the provider
|
||||
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
|
||||
// not sure if goth is thread safe (?) when using multiple providers
|
||||
request.Header.Set(ProviderHeaderKey, source.authSource.Name)
|
||||
request.Header.Set(ProviderHeaderKey, source.AuthSource.Name)
|
||||
|
||||
// don't use the default gothic begin handler to prevent issues when some error occurs
|
||||
// normally the gothic library will write some custom stuff to the response instead of our own nice error page
|
||||
@ -33,7 +33,7 @@ func (source *Source) Callout(request *http.Request, response http.ResponseWrite
|
||||
// this will trigger a new authentication request, but because we save it in the session we can use that
|
||||
func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
|
||||
// not sure if goth is thread safe (?) when using multiple providers
|
||||
request.Header.Set(ProviderHeaderKey, source.authSource.Name)
|
||||
request.Header.Set(ProviderHeaderKey, source.AuthSource.Name)
|
||||
|
||||
gothRWMutex.RLock()
|
||||
defer gothRWMutex.RUnlock()
|
||||
|
@ -9,13 +9,13 @@ import (
|
||||
|
||||
// RegisterSource causes an OAuth2 configuration to be registered
|
||||
func (source *Source) RegisterSource() error {
|
||||
err := RegisterProviderWithGothic(source.authSource.Name, source)
|
||||
return wrapOpenIDConnectInitializeError(err, source.authSource.Name, source)
|
||||
err := RegisterProviderWithGothic(source.AuthSource.Name, source)
|
||||
return wrapOpenIDConnectInitializeError(err, source.AuthSource.Name, source)
|
||||
}
|
||||
|
||||
// UnregisterSource causes an OAuth2 configuration to be unregistered
|
||||
func (source *Source) UnregisterSource() error {
|
||||
RemoveProviderFromGothic(source.authSource.Name)
|
||||
RemoveProviderFromGothic(source.AuthSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -18,27 +18,27 @@ import (
|
||||
|
||||
// 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)
|
||||
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)
|
||||
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.AuthSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
provider, err := createProvider(source.authSource.Name, source)
|
||||
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)
|
||||
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,
|
||||
LoginSourceID: source.AuthSource.ID,
|
||||
}
|
||||
|
||||
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
|
||||
@ -77,7 +77,7 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us
|
||||
// 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)
|
||||
log.Info("SyncExternalUsers[%s] disabling user %d", source.AuthSource.Name, user.ID)
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if hasUser {
|
||||
|
@ -18,19 +18,21 @@ func TestSource(t *testing.T) {
|
||||
|
||||
source := &Source{
|
||||
Provider: "fake",
|
||||
authSource: &auth.Source{
|
||||
ID: 12,
|
||||
Type: auth.OAuth2,
|
||||
Name: "fake",
|
||||
IsActive: true,
|
||||
IsSyncEnabled: true,
|
||||
ConfigBase: auth.ConfigBase{
|
||||
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,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
Name: "test",
|
||||
Email: "external@example.com",
|
||||
}
|
||||
@ -47,7 +49,7 @@ func TestSource(t *testing.T) {
|
||||
err = user_model.LinkExternalToUser(t.Context(), user, e)
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider, err := createProvider(source.authSource.Name, source)
|
||||
provider, err := createProvider(source.AuthSource.Name, source)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("refresh", func(t *testing.T) {
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
auth_model.Config
|
||||
auth_model.SourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &pam.Source{}
|
||||
|
@ -17,12 +17,10 @@ import (
|
||||
|
||||
// Source holds configuration for the PAM login source.
|
||||
type Source struct {
|
||||
ServiceName string // pam service (e.g. system-auth)
|
||||
EmailDomain string
|
||||
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
// reference to the authSource
|
||||
authSource *auth.Source
|
||||
ServiceName string // pam service (e.g. system-auth)
|
||||
EmailDomain string
|
||||
}
|
||||
|
||||
// FromDB fills up a PAMConfig from serialized format.
|
||||
@ -35,11 +33,6 @@ func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// SetAuthSource sets the related AuthSource
|
||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
||||
source.authSource = authSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.PAM, &Source{})
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
Email: email,
|
||||
Passwd: password,
|
||||
LoginType: auth.PAM,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
LoginName: userName, // This is what the user typed in
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
@ -69,8 +69,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
|
||||
func (source *Source) IsSkipLocalTwoFA() bool {
|
||||
return source.SkipLocalTwoFA
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ type sourceInterface interface {
|
||||
auth_model.SkipVerifiable
|
||||
auth_model.HasTLSer
|
||||
auth_model.UseTLSer
|
||||
auth_model.SourceSettable
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &smtp.Source{}
|
||||
|
@ -17,6 +17,8 @@ import (
|
||||
|
||||
// Source holds configuration for the SMTP login source.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
Auth string
|
||||
Host string
|
||||
Port int
|
||||
@ -25,10 +27,6 @@ type Source struct {
|
||||
SkipVerify bool
|
||||
HeloHostname string
|
||||
DisableHelo bool
|
||||
SkipLocalTwoFA bool `json:",omitempty"`
|
||||
|
||||
// reference to the authSource
|
||||
authSource *auth.Source
|
||||
}
|
||||
|
||||
// FromDB fills up an SMTPConfig from serialized format.
|
||||
@ -56,11 +54,6 @@ func (source *Source) UseTLS() bool {
|
||||
return source.ForceSMTPS || source.Port == 465
|
||||
}
|
||||
|
||||
// SetAuthSource sets the related AuthSource
|
||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
||||
source.authSource = authSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.SMTP, &Source{})
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
Email: userName,
|
||||
Passwd: password,
|
||||
LoginType: auth_model.SMTP,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
LoginName: userName,
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
@ -85,8 +85,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
|
||||
func (source *Source) IsSkipLocalTwoFA() bool {
|
||||
return source.SkipLocalTwoFA
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ import (
|
||||
|
||||
// Source holds configuration for SSPI single sign-on.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
AutoCreateUsers bool
|
||||
AutoActivateUsers bool
|
||||
StripDomainNames bool
|
||||
|
@ -196,6 +196,8 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
|
||||
ctx.Data["SystemConfig"] = setting.Config()
|
||||
|
||||
ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth()
|
||||
|
||||
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
||||
@ -209,6 +211,13 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) DoerNeedTwoFactorAuth() bool {
|
||||
if !setting.TwoFactorAuthEnforced {
|
||||
return false
|
||||
}
|
||||
return ctx.Session.Get(session.KeyUserHasTwoFactorAuth) == false
|
||||
}
|
||||
|
||||
// HasError returns true if error occurs in form validation.
|
||||
// Attention: this function changes ctx.Data and ctx.Flash
|
||||
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
|
||||
|
@ -340,10 +340,14 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
if ctx.DoerNeedTwoFactorAuth() {
|
||||
ctx.Repo.Permission = access_model.PermissionNoAccess()
|
||||
} else {
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
|
||||
|
@ -14,9 +14,11 @@ import (
|
||||
|
||||
// AuthenticationForm form for authentication
|
||||
type AuthenticationForm struct {
|
||||
ID int64
|
||||
Type int `binding:"Range(2,7)"`
|
||||
Name string `binding:"Required;MaxSize(30)"`
|
||||
ID int64
|
||||
Type int `binding:"Range(2,7)"`
|
||||
Name string `binding:"Required;MaxSize(30)"`
|
||||
TwoFactorPolicy string
|
||||
|
||||
Host string
|
||||
Port int
|
||||
BindDN string
|
||||
@ -74,7 +76,6 @@ type AuthenticationForm struct {
|
||||
Oauth2RestrictedGroup string
|
||||
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||
Oauth2GroupTeamMapRemoval bool
|
||||
SkipLocalTwoFA bool
|
||||
SSPIAutoCreateUsers bool
|
||||
SSPIAutoActivateUsers bool
|
||||
SSPIStripDomainNames bool
|
||||
|
@ -17,6 +17,13 @@
|
||||
<label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label>
|
||||
<input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label ><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
|
||||
<input name="two_factor_policy" type="checkbox" value="skip" {{if eq .Source.TwoFactorPolicy "skip"}}checked{{end}}>
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LDAP and DLDAP -->
|
||||
{{if or .Source.IsLDAP .Source.IsDLDAP}}
|
||||
@ -159,13 +166,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="optional field">
|
||||
<div class="ui checkbox">
|
||||
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
|
||||
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label for="allow_deactivate_all"><strong>{{ctx.Locale.Tr "admin.auths.allow_deactivate_all"}}</strong></label>
|
||||
@ -227,13 +227,6 @@
|
||||
<input id="allowed_domains" name="allowed_domains" value="{{$cfg.AllowedDomains}}">
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}</p>
|
||||
</div>
|
||||
<div class="optional field">
|
||||
<div class="ui checkbox">
|
||||
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
|
||||
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- PAM -->
|
||||
@ -247,13 +240,6 @@
|
||||
<label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label>
|
||||
<input id="pam_email_domain" name="pam_email_domain" value="{{$cfg.EmailDomain}}">
|
||||
</div>
|
||||
<div class="optional field">
|
||||
<div class="ui checkbox">
|
||||
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
|
||||
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- OAuth2 -->
|
||||
@ -288,13 +274,6 @@
|
||||
<label for="open_id_connect_auto_discovery_url">{{ctx.Locale.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
|
||||
<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{$cfg.OpenIDConnectAutoDiscoveryURL}}">
|
||||
</div>
|
||||
<div class="optional field">
|
||||
<div class="ui checkbox">
|
||||
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
|
||||
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
|
||||
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oauth2_use_custom_url inline field">
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{ctx.Locale.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>
|
||||
|
@ -18,3 +18,8 @@
|
||||
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- if .ShowTwoFactorRequiredMessage -}}
|
||||
<div class="ui negative message flash-message flash-error">
|
||||
<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}}</a></p>
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
@ -2,6 +2,7 @@
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
|
||||
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="status-page-error">
|
||||
<div class="status-page-error-title">404 Not Found</div>
|
||||
<div class="tw-text-center">
|
||||
|
@ -94,7 +94,7 @@ func TestBasicAuthWithWebAuthn(t *testing.T) {
|
||||
}
|
||||
var userParsed userResponse
|
||||
DecodeJSON(t, resp, &userParsed)
|
||||
assert.Equal(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message)
|
||||
assert.Equal(t, "basic authorization is not allowed while WebAuthn enrolled", userParsed.Message)
|
||||
|
||||
// user32 has webauthn enrolled, he can't request git protocol with basic auth
|
||||
req = NewRequest(t, "GET", "/user2/repo1/info/refs")
|
||||
|
Loading…
x
Reference in New Issue
Block a user