diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index 274ec181d1..d2eeb7c0d6 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -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 } diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index 8e6239ac33..be5345d992 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -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) } diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_stmp.go index d724746905..babcf78cea 100644 --- a/cmd/admin_auth_stmp.go +++ b/cmd/admin_auth_stmp.go @@ -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) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 42d181a00a..a7476ad1be 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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 = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/go.mod b/go.mod index bd234a1b61..892f03dc1c 100644 --- a/go.mod +++ b/go.mod @@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 +replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 + exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible diff --git a/go.sum b/go.sum index 9d71981e16..c9dcc235aa 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8= gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 524224f070..757bd13acd 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -30,6 +30,25 @@ const ( ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted ) +func (status ArtifactStatus) ToString() string { + switch status { + case ArtifactStatusUploadPending: + return "upload is not yet completed" + case ArtifactStatusUploadConfirmed: + return "upload is completed" + case ArtifactStatusUploadError: + return "upload failed" + case ArtifactStatusExpired: + return "expired" + case ArtifactStatusPendingDeletion: + return "pending deletion" + case ArtifactStatusDeleted: + return "deleted" + default: + return "unknown" + } +} + func init() { db.RegisterModel(new(ActionArtifact)) } diff --git a/models/auth/source.go b/models/auth/source.go index a3a250cd91..7d7bc0f03c 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -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 { diff --git a/models/auth/source_test.go b/models/auth/source_test.go index 84aede0a6b..64c7460b64 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -19,6 +19,8 @@ import ( ) type TestSource struct { + auth_model.ConfigBase + Provider string ClientID string ClientSecret string diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index d0c341a192..200ce7c7c0 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -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) +} diff --git a/models/fixtures/action_artifact.yml b/models/fixtures/action_artifact.yml index 485474108f..1b00daf198 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -11,6 +11,24 @@ content_encoding: "" artifact_path: "abc.txt" artifact_name: "artifact-download" + status: 2 + created_unix: 1712338649 + updated_unix: 1712338649 + expired_unix: 1720114649 + +- + id: 2 + run_id: 791 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "30/20/1712348022422036662.chunk" + artifact_path: "abc.txt" + artifact_name: "artifact-download-incomplete" status: 1 created_unix: 1712338649 updated_unix: 1712338649 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 31b035eb31..176372486e 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 } diff --git a/models/migrations/v1_24/v320.go b/models/migrations/v1_24/v320.go new file mode 100644 index 0000000000..1d34444826 --- /dev/null +++ b/models/migrations/v1_24/v320.go @@ -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 +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 9d0c9f0077..45efb192c8 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -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} +} diff --git a/modules/fileicon/basic.go b/modules/fileicon/basic.go index 040a8e87de..9c513ccbd9 100644 --- a/modules/fileicon/basic.go +++ b/modules/fileicon/basic.go @@ -6,22 +6,26 @@ package fileicon import ( "html/template" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" ) -func BasicThemeIcon(entry *git.TreeEntry) template.HTML { +func BasicEntryIconName(entry *EntryInfo) string { svgName := "octicon-file" switch { - case entry.IsLink(): + case entry.EntryMode.IsLink(): svgName = "octicon-file-symlink-file" - if te, err := entry.FollowLink(); err == nil && te.IsDir() { + if entry.SymlinkToMode.IsDir() { svgName = "octicon-file-directory-symlink" } - case entry.IsDir(): - svgName = "octicon-file-directory-fill" - case entry.IsSubModule(): + case entry.EntryMode.IsDir(): + svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill") + case entry.EntryMode.IsSubModule(): svgName = "octicon-file-submodule" } - return svg.RenderHTML(svgName) + return svgName +} + +func BasicEntryIconHTML(entry *EntryInfo) template.HTML { + return svg.RenderHTML(BasicEntryIconName(entry)) } diff --git a/modules/fileicon/entry.go b/modules/fileicon/entry.go new file mode 100644 index 0000000000..e4ded363e5 --- /dev/null +++ b/modules/fileicon/entry.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import "code.gitea.io/gitea/modules/git" + +type EntryInfo struct { + FullName string + EntryMode git.EntryMode + SymlinkToMode git.EntryMode + IsOpen bool +} + +func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { + ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} + if gitEntry.IsLink() { + if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { + ret.SymlinkToMode = te.Mode() + } + } + return ret +} + +func EntryInfoFolder() *EntryInfo { + return &EntryInfo{EntryMode: git.EntryModeTree} +} + +func EntryInfoFolderOpen() *EntryInfo { + return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true} +} diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index 557f7ca9e4..449f527ee8 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -9,11 +9,12 @@ import ( "strings" "sync" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" ) type materialIconRulesData struct { @@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, } svgID := "svg-mfi-" + name svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` + svgHTML := template.HTML(``) } -func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { +func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML { if m.rules == nil { - return BasicThemeIcon(entry) + return BasicEntryIconHTML(entry) } - if entry.IsLink() { - if te, err := entry.FollowLink(); err == nil && te.IsDir() { + if entry.EntryMode.IsLink() { + if entry.SymlinkToMode.IsDir() { // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink") } return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them } - name := m.findIconNameByGit(entry) - // the material icon pack's "folder" icon doesn't look good, so use our built-in one - // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work - if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" { - // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work - extraClass := "octicon-file" - switch { - case entry.IsDir(): - extraClass = "octicon-file-directory-fill" - case entry.IsSubModule(): - extraClass = "octicon-file-submodule" + name := m.FindIconName(entry) + iconSVG := m.svgs[name] + if iconSVG == "" { + name = "file" + if entry.EntryMode.IsDir() { + name = util.Iif(entry.IsOpen, "folder-open", "folder") + } + iconSVG = m.svgs[name] + if iconSVG == "" { + setting.PanicInDevOrTesting("missing file icon for %s", name) } - return m.renderFileIconSVG(p, name, iconSVG, extraClass) } - // TODO: use an interface or wrapper for git.Entry to make the code testable. - return BasicThemeIcon(entry) + + // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work + extraClass := "octicon-file" + switch { + case entry.EntryMode.IsDir(): + extraClass = BasicEntryIconName(entry) + case entry.EntryMode.IsSubModule(): + extraClass = "octicon-file-submodule" + } + return m.renderFileIconSVG(p, name, iconSVG, extraClass) } func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { @@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { return "" } -func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { - fileNameLower := strings.ToLower(path.Base(name)) - if isDir { +func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string { + if entry.EntryMode.IsSubModule() { + return "folder-git" + } + + fileNameLower := strings.ToLower(path.Base(entry.FullName)) + if entry.EntryMode.IsDir() { if s, ok := m.rules.FolderNames[fileNameLower]; ok { return s } - return "folder" + return util.Iif(entry.IsOpen, "folder-open", "folder") } if s, ok := m.rules.FileNames[fileNameLower]; ok { @@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { return "file" } - -func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string { - if entry.IsSubModule() { - return "folder-git" - } - return m.FindIconName(entry.Name(), entry.IsDir()) -} diff --git a/modules/fileicon/material_test.go b/modules/fileicon/material_test.go index f36385aaf3..68353d2189 100644 --- a/modules/fileicon/material_test.go +++ b/modules/fileicon/material_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/fileicon" + "code.gitea.io/gitea/modules/git" "github.com/stretchr/testify/assert" ) @@ -19,8 +20,8 @@ func TestMain(m *testing.M) { func TestFindIconName(t *testing.T) { unittest.PrepareTestEnv(t) p := fileicon.DefaultMaterialIconProvider() - assert.Equal(t, "php", p.FindIconName("foo.php", false)) - assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) - assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) - assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob})) } diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go index 1d014693fd..8ed86b9ac0 100644 --- a/modules/fileicon/render.go +++ b/modules/fileicon/render.go @@ -7,7 +7,6 @@ import ( "html/template" "strings" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" ) @@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { return template.HTML(sb.String()) } -// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module - -func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { +func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { if setting.UI.FileIconTheme == "material" { - return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) + return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } - return BasicThemeIcon(entry) -} - -func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { - // TODO: add "open icon" support - if setting.UI.FileIconTheme == "material" { - return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) - } - return BasicThemeIcon(entry) + return BasicEntryIconHTML(entry) } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index 1193bec4f1..d815a8bc2e 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -30,6 +30,31 @@ func (e EntryMode) String() string { return strconv.FormatInt(int64(e), 8) } +// IsSubModule if the entry is a sub module +func (e EntryMode) IsSubModule() bool { + return e == EntryModeCommit +} + +// IsDir if the entry is a sub dir +func (e EntryMode) IsDir() bool { + return e == EntryModeTree +} + +// IsLink if the entry is a symlink +func (e EntryMode) IsLink() bool { + return e == EntryModeSymlink +} + +// IsRegular if the entry is a regular file +func (e EntryMode) IsRegular() bool { + return e == EntryModeBlob +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (e EntryMode) IsExecutable() bool { + return e == EntryModeExec +} + func ParseEntryMode(mode string) (EntryMode, error) { switch mode { case "000000": diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 81fb638d56..0c0e1835f1 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 { // IsSubModule if the entry is a sub module func (te *TreeEntry) IsSubModule() bool { - return te.entryMode == EntryModeCommit + return te.entryMode.IsSubModule() } // IsDir if the entry is a sub dir func (te *TreeEntry) IsDir() bool { - return te.entryMode == EntryModeTree + return te.entryMode.IsDir() } // IsLink if the entry is a symlink func (te *TreeEntry) IsLink() bool { - return te.entryMode == EntryModeSymlink + return te.entryMode.IsLink() } // IsRegular if the entry is a regular file func (te *TreeEntry) IsRegular() bool { - return te.entryMode == EntryModeBlob + return te.entryMode.IsRegular() } // IsExecutable if the entry is an executable file (not necessarily binary) func (te *TreeEntry) IsExecutable() bool { - return te.entryMode == EntryModeExec + return te.entryMode.IsExecutable() } // Blob returns the blob object the entry diff --git a/modules/session/key.go b/modules/session/key.go new file mode 100644 index 0000000000..c3da997c67 --- /dev/null +++ b/modules/session/key.go @@ -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" +) diff --git a/modules/setting/security.go b/modules/setting/security.go index 2f798b75c7..3ae4c005c7 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9928b3588a..a8fabc9ca1 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 0832e52f55..6473659e5c 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -337,7 +337,10 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ + RunID: runID, + Status: int(actions.ArtifactStatusUploadConfirmed), + }) if err != nil { log.Error("Error getting artifacts: %v", err) ctx.HTTPError(http.StatusInternalServerError, err.Error()) @@ -402,6 +405,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ RunID: runID, ArtifactName: itemPath, + Status: int(actions.ArtifactStatusUploadConfirmed), }) if err != nil { log.Error("Error getting artifacts: %v", err) @@ -473,6 +477,11 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusBadRequest) return } + if artifact.Status != actions.ArtifactStatusUploadConfirmed { + log.Error("Error artifact not found: %s", artifact.Status.ToString()) + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") + return + } fd, err := ar.fs.Open(artifact.StoragePath) if err != nil { diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 9fb0a31549..e9e9fc6393 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -448,17 +448,15 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ + RunID: runID, + Status: int(actions.ArtifactStatusUploadConfirmed), + }) if err != nil { log.Error("Error getting artifacts: %v", err) ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } - if len(artifacts) == 0 { - log.Debug("[artifact] handleListArtifacts, no artifacts") - ctx.HTTPError(http.StatusNotFound) - return - } list := []*ListArtifactsResponse_MonolithArtifact{} @@ -510,6 +508,11 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } + if artifact.Status != actions.ArtifactStatusUploadConfirmed { + log.Error("Error artifact not found: %s", artifact.Status.ToString()) + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") + return + } respData := GetSignedArtifactURLResponse{} @@ -538,6 +541,11 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } + if artifact.Status != actions.ArtifactStatusUploadConfirmed { + log.Error("Error artifact not found: %s", artifact.Status.ToString()) + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") + return + } file, _ := r.fs.Open(artifact.StoragePath) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 58ae8ec90a..b98863b418 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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() { diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 2b3bf1f77d..80d554b6e3 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -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 diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index fe363fe90a..d15d33dfd4 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -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 } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 1de8d7e8a3..69b9d285b7 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -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 + "/" diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 94a8bec565..96c1dcf358 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -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 diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 973d68d45c..ae5baa9c47 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -21,6 +21,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" @@ -369,7 +370,11 @@ func Diff(ctx *context.Context) { return } - ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) + renderedIconPool := fileicon.NewRenderedIconPool() + ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil) + ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) + ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) + ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 13fbac981c..34d9710acb 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" csv_module "code.gitea.io/gitea/modules/csv" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" @@ -639,7 +640,11 @@ func PrepareCompareDiff( return false } - ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) + renderedIconPool := fileicon.NewRenderedIconPool() + ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil) + ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) + ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) + ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 15c9658fa8..6a63240269 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/emoji" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" issue_template "code.gitea.io/gitea/modules/issue/template" @@ -823,7 +824,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi if reviewState != nil { filesViewedState = reviewState.UpdatedFiles } - ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState) + + renderedIconPool := fileicon.NewRenderedIconPool() + ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState) + ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) + ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) + ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } ctx.Data["Diff"] = diff diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 994b2d0c0a..0248a0627b 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -4,6 +4,7 @@ package repo import ( + "html/template" "net/http" "strings" @@ -67,7 +68,7 @@ type WebDiffFileItem struct { EntryMode string IsViewed bool Children []*WebDiffFileItem - // TODO: add icon support in the future + FileIcon template.HTML } // WebDiffFileTree is used by frontend, check the field names in frontend before changing @@ -77,7 +78,7 @@ type WebDiffFileTree struct { // transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering // it also takes a map of file names to their viewed state, which is used to mark files as viewed -func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { +func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot} addItem := func(item *WebDiffFileItem) { var parentPath string @@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.NameHash = git.HashFilePathForWebUI(item.FullName) + item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode}) switch file.HeadMode { case git.EntryModeTree: diff --git a/routers/web/repo/treelist_test.go b/routers/web/repo/treelist_test.go index 2dff64a028..94ba60661b 100644 --- a/routers/web/repo/treelist_test.go +++ b/routers/web/repo/treelist_test.go @@ -4,9 +4,11 @@ package repo import ( + "html/template" "testing" pull_model "code.gitea.io/gitea/models/pull" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/gitdiff" @@ -14,7 +16,8 @@ import ( ) func TestTransformDiffTreeForWeb(t *testing.T) { - ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ + renderedIconPool := fileicon.NewRenderedIconPool() + ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ { Status: "changed", HeadPath: "dir-a/dir-a-x/file-deep", @@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) { "dir-a/dir-a-x/file-deep": pull_model.Viewed, }) + mockIconForFile := func(id string) template.HTML { + return template.HTML(``) + } assert.Equal(t, WebDiffFileTree{ TreeRoot: WebDiffFileItem{ Children: []*WebDiffFileItem{ @@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) { NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b", DiffStatus: "changed", IsViewed: true, + FileIcon: mockIconForFile(`svg-mfi-file`), }, }, }, @@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) { FullName: "file1", NameHash: "60b27f004e454aca81b0480209cce5081ec52390", DiffStatus: "added", + FileIcon: mockIconForFile(`svg-mfi-file`), }, }, }, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 77240f0431..2f01434684 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) { renderedIconPool := fileicon.NewRenderedIconPool() fileIcons := map[string]template.HTML{} for _, f := range files { - fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry) + fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry)) } + fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) ctx.Data["FileIcons"] = fileIcons ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go index e5315efc74..e5e23c820c 100644 --- a/routers/web/user/setting/security/2fa.go +++ b/routers/web/user/setting/security/2fa.go @@ -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 } diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go index 63721343df..eb9f46af52 100644 --- a/routers/web/user/setting/security/webauthn.go +++ b/routers/web/user/setting/security/webauthn.go @@ -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) } diff --git a/services/auth/basic.go b/services/auth/basic.go index e22b9e1eb7..a208590d7b 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -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 { diff --git a/services/auth/interface.go b/services/auth/interface.go index 275b4dd56c..e7eccecea0 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -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 diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go index bb2270cbd6..90baa61f5b 100644 --- a/services/auth/source/db/source.go +++ b/services/auth/source/db/source.go @@ -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 { diff --git a/services/auth/source/ldap/assert_interface_test.go b/services/auth/source/ldap/assert_interface_test.go index 33347687dc..8e8accd668 100644 --- a/services/auth/source/ldap/assert_interface_test.go +++ b/services/auth/source/ldap/assert_interface_test.go @@ -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{} diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go index 963cdba7c2..d49dbc45ce 100644 --- a/services/auth/source/ldap/source.go +++ b/services/auth/source/ldap/source.go @@ -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{}) diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 7aee761d0d..a2e8c2b86a 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -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 -} diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index ff36db2955..678b6b2b62 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -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) } } } diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go index 56fe0e4aa8..d870ac1dcd 100644 --- a/services/auth/source/oauth2/assert_interface_test.go +++ b/services/auth/source/oauth2/assert_interface_test.go @@ -14,7 +14,6 @@ import ( type sourceInterface interface { auth_model.Config - auth_model.SourceSettable auth_model.RegisterableSource auth.PasswordAuthenticator } diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 3454c9ad55..08837de377 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -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{}) } diff --git a/services/auth/source/oauth2/source_callout.go b/services/auth/source/oauth2/source_callout.go index 8d70bee248..f09d25c772 100644 --- a/services/auth/source/oauth2/source_callout.go +++ b/services/auth/source/oauth2/source_callout.go @@ -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() diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go index 82a36acaa6..12da56c11b 100644 --- a/services/auth/source/oauth2/source_register.go +++ b/services/auth/source/oauth2/source_register.go @@ -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 } diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 5e30313c8f..c2e3dfb1a8 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -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 { diff --git a/services/auth/source/oauth2/source_sync_test.go b/services/auth/source/oauth2/source_sync_test.go index 08d841cc90..2927f3634b 100644 --- a/services/auth/source/oauth2/source_sync_test.go +++ b/services/auth/source/oauth2/source_sync_test.go @@ -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) { diff --git a/services/auth/source/pam/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go index 8e7648b8d3..908d097d96 100644 --- a/services/auth/source/pam/assert_interface_test.go +++ b/services/auth/source/pam/assert_interface_test.go @@ -15,7 +15,6 @@ import ( type sourceInterface interface { auth.PasswordAuthenticator auth_model.Config - auth_model.SourceSettable } var _ (sourceInterface) = &pam.Source{} diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go index 96b182e185..d1db6db9b7 100644 --- a/services/auth/source/pam/source.go +++ b/services/auth/source/pam/source.go @@ -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{}) } diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go index 6fd02dc29f..db7c6aab96 100644 --- a/services/auth/source/pam/source_authenticate.go +++ b/services/auth/source/pam/source_authenticate.go @@ -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 -} diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go index 6c9cde66e1..56edad0c71 100644 --- a/services/auth/source/smtp/assert_interface_test.go +++ b/services/auth/source/smtp/assert_interface_test.go @@ -18,7 +18,6 @@ type sourceInterface interface { auth_model.SkipVerifiable auth_model.HasTLSer auth_model.UseTLSer - auth_model.SourceSettable } var _ (sourceInterface) = &smtp.Source{} diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go index 2a648e421e..2ae81ad4f2 100644 --- a/services/auth/source/smtp/source.go +++ b/services/auth/source/smtp/source.go @@ -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{}) } diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go index b2e94933a6..b8e668f5f9 100644 --- a/services/auth/source/smtp/source_authenticate.go +++ b/services/auth/source/smtp/source_authenticate.go @@ -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 -} diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go index bdd6ef451c..3b7b5cb033 100644 --- a/services/auth/source/sspi/source.go +++ b/services/auth/source/sspi/source.go @@ -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 diff --git a/services/context/context.go b/services/context/context.go index 3c0ac54fc1..7f623f85bd 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -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. diff --git a/services/context/repo.go b/services/context/repo.go index 6f5c772f5e..127d313258 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -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) { diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index c9f3182b3a..a8f97572b1 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -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 diff --git a/services/issue/pull.go b/services/issue/pull.go index c9d32000af..3543b05b18 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -48,10 +48,6 @@ func IsCodeOwnerFile(f string) bool { } func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { - return PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, "", "") // no commit is provided, then it uses PR's base&head branch -} - -func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_model.PullRequest, startCommitID, endCommitID string) ([]*ReviewRequestNotifier, error) { if err := pr.LoadIssue(ctx); err != nil { return nil, err } @@ -100,19 +96,15 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m return nil, nil } - if startCommitID == "" && endCommitID == "" { - // get the mergebase - mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) - if err != nil { - return nil, err - } - startCommitID = mergeBase - endCommitID = pr.GetGitRefName() + // get the mergebase + mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) + if err != nil { + return nil, err } // https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed // between the merge base and the head commit but not the base branch and the head commit - changedFiles, err := repo.GetFilesChangedBetween(startCommitID, endCommitID) + changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName()) if err != nil { return nil, err } @@ -138,8 +130,23 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m return nil, err } + // load all reviews from database + latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID) + if err != nil { + return nil, err + } + + contain := func(list issues_model.ReviewList, u *user_model.User) bool { + for _, review := range list { + if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID { + return true + } + } + return false + } + for _, u := range uniqUsers { - if u.ID != issue.Poster.ID { + if u.ID != issue.Poster.ID && !contain(latestReivews, u) { comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster) if err != nil { log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) @@ -155,6 +162,7 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m }) } } + for _, t := range uniqTeams { comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster) if err != nil { diff --git a/services/pull/pull.go b/services/pull/pull.go index e3053409b2..81be797832 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -463,12 +463,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) { } if !pr.IsWorkInProgress(ctx) { - var reviewNotifiers []*issue_service.ReviewRequestNotifier - if opts.IsForcePush { - reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr) - } else { - reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, opts.OldCommitID, opts.NewCommitID) - } + reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr) if err != nil { log.Error("PullRequestCodeOwnersReview: %v", err) } diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index faeb85a046..8427fcbacc 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -165,19 +165,11 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re FullPath: path.Join(parentDir, entry.Name()), } - if entry.IsLink() { - // TODO: symlink to a folder or a file, the icon differs - target, err := entry.FollowLink() - if err == nil { - _ = target.IsDir() - // if target.IsDir() { } else { } - } - } - - if node.EntryIcon == "" { - node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry) - // TODO: no open icon support yet - // node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry) + entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry) + node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) + if entryInfo.EntryMode.IsDir() { + entryInfo.IsOpen = true + node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) } if node.EntryMode == "commit" { diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index 2657c49977..a53f342d40 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -71,14 +71,18 @@ func TestGetTreeViewNodes(t *testing.T) { mockIconForFolder := func(id string) template.HTML { return template.HTML(``) } + mockOpenIconForFolder := func(id string) template.HTML { + return template.HTML(``) + } treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { - EntryName: "docs", - EntryMode: "tree", - FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), + EntryName: "docs", + EntryMode: "tree", + FullPath: "docs", + EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), + EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), }, }, treeNodes) @@ -86,10 +90,11 @@ func TestGetTreeViewNodes(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { - EntryName: "docs", - EntryMode: "tree", - FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), + EntryName: "docs", + EntryMode: "tree", + FullPath: "docs", + EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), + EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), Children: []*TreeViewNode{ { EntryName: "README.md", diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 660f0d0881..15683307ed 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -17,6 +17,13 @@ +
{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
+{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
-{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}
{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
-{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
-{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
-{{.Flash.WarningMsg | SanitizeHTML}}