mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +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:
@@ -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}
|
||||
}
|
||||
|
Reference in New Issue
Block a user