mirror of https://github.com/go-gitea/gitea.git
Merge branch 'go-gitea:main' into main
This commit is contained in:
commit
4e2434b438
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
@ -210,8 +211,8 @@ func newAuthService() *authService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAuthSource assigns values on authSource according to command line flags.
|
// parseAuthSourceLdap assigns values on authSource according to command line flags.
|
||||||
func parseAuthSource(c *cli.Context, authSource *auth.Source) {
|
func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
|
||||||
if c.IsSet("name") {
|
if c.IsSet("name") {
|
||||||
authSource.Name = c.String("name")
|
authSource.Name = c.String("name")
|
||||||
}
|
}
|
||||||
|
|
@ -227,6 +228,7 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) {
|
||||||
if c.IsSet("disable-synchronize-users") {
|
if c.IsSet("disable-synchronize-users") {
|
||||||
authSource.IsSyncEnabled = !c.Bool("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.
|
// 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") {
|
if c.IsSet("allow-deactivate-all") {
|
||||||
config.AllowDeactivateAll = c.Bool("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") {
|
if c.IsSet("enable-groups") {
|
||||||
config.GroupsEnabled = c.Bool("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 {
|
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -398,7 +397,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parseAuthSource(c, authSource)
|
parseAuthSourceLdap(c, authSource)
|
||||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||||
return err
|
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 {
|
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +448,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parseAuthSource(c, authSource)
|
parseAuthSourceLdap(c, authSource)
|
||||||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
@ -156,7 +157,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||||
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
|
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
|
||||||
CustomURLMapping: customURLMapping,
|
CustomURLMapping: customURLMapping,
|
||||||
IconURL: c.String("icon-url"),
|
IconURL: c.String("icon-url"),
|
||||||
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
|
|
||||||
Scopes: c.StringSlice("scopes"),
|
Scopes: c.StringSlice("scopes"),
|
||||||
RequiredClaimName: c.String("required-claim-name"),
|
RequiredClaimName: c.String("required-claim-name"),
|
||||||
RequiredClaimValue: c.String("required-claim-value"),
|
RequiredClaimValue: c.String("required-claim-value"),
|
||||||
|
|
@ -185,10 +185,11 @@ func runAddOauth(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||||
Type: auth_model.OAuth2,
|
Type: auth_model.OAuth2,
|
||||||
Name: c.String("name"),
|
Name: c.String("name"),
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
Cfg: config,
|
Cfg: config,
|
||||||
|
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,6 +295,6 @@ func runUpdateOauth(c *cli.Context) error {
|
||||||
|
|
||||||
oAuth2Config.CustomURLMapping = customURLMapping
|
oAuth2Config.CustomURLMapping = customURLMapping
|
||||||
source.Cfg = oAuth2Config
|
source.Cfg = oAuth2Config
|
||||||
|
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||||
return auth_model.UpdateSource(ctx, source)
|
return auth_model.UpdateSource(ctx, source)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,6 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
|
||||||
if c.IsSet("disable-helo") {
|
if c.IsSet("disable-helo") {
|
||||||
conf.DisableHelo = c.Bool("disable-helo")
|
conf.DisableHelo = c.Bool("disable-helo")
|
||||||
}
|
}
|
||||||
if c.IsSet("skip-local-2fa") {
|
|
||||||
conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,10 +153,11 @@ func runAddSMTP(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||||
Type: auth_model.SMTP,
|
Type: auth_model.SMTP,
|
||||||
Name: c.String("name"),
|
Name: c.String("name"),
|
||||||
IsActive: active,
|
IsActive: active,
|
||||||
Cfg: &smtpConfig,
|
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.Cfg = smtpConfig
|
||||||
|
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
|
||||||
return auth_model.UpdateSource(ctx, source)
|
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.
|
;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
|
||||||
;; RECORD_USER_SIGNUP_METADATA = false
|
;; 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 =
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
||||||
2
go.mod
2
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
|
// 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 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 v3.2.0+incompatible
|
||||||
|
|
||||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||||
|
|
|
||||||
4
go.sum
4
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=
|
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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
|
||||||
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
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 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
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 h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
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=
|
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,25 @@ const (
|
||||||
ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
|
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() {
|
func init() {
|
||||||
db.RegisterModel(new(ActionArtifact))
|
db.RegisterModel(new(ActionArtifact))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,15 @@ var Names = map[Type]string{
|
||||||
// Config represents login config as far as the db is concerned
|
// Config represents login config as far as the db is concerned
|
||||||
type Config interface {
|
type Config interface {
|
||||||
convert.Conversion
|
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
|
// 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.
|
// Source represents an external way for authorizing users.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
Type Type
|
Type Type
|
||||||
Name string `xorm:"UNIQUE"`
|
Name string `xorm:"UNIQUE"`
|
||||||
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
Cfg convert.Conversion `xorm:"TEXT"`
|
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
|
||||||
|
Cfg Config `xorm:"TEXT"`
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
|
@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
source.Cfg = constructor()
|
source.Cfg = constructor()
|
||||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
source.Cfg.SetAuthSource(source)
|
||||||
settable.SetAuthSource(source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool {
|
||||||
return ok && skipVerifiable.IsSkipVerify()
|
return ok && skipVerifiable.IsSkipVerify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (source *Source) TwoFactorShouldSkip() bool {
|
||||||
|
return source.TwoFactorPolicy == "skip"
|
||||||
|
}
|
||||||
|
|
||||||
// CreateSource inserts a AuthSource in the DB if not already
|
// CreateSource inserts a AuthSource in the DB if not already
|
||||||
// existing with the given name.
|
// existing with the given name.
|
||||||
func CreateSource(ctx context.Context, source *Source) error {
|
func CreateSource(ctx context.Context, source *Source) error {
|
||||||
|
|
@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
source.Cfg.SetAuthSource(source)
|
||||||
settable.SetAuthSource(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerableSource, ok := source.Cfg.(RegisterableSource)
|
registerableSource, ok := source.Cfg.(RegisterableSource)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if settable, ok := source.Cfg.(SourceSettable); ok {
|
source.Cfg.SetAuthSource(source)
|
||||||
settable.SetAuthSource(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerableSource, ok := source.Cfg.(RegisterableSource)
|
registerableSource, ok := source.Cfg.(RegisterableSource)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestSource struct {
|
type TestSource struct {
|
||||||
|
auth_model.ConfigBase
|
||||||
|
|
||||||
Provider string
|
Provider string
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
|
|
|
||||||
|
|
@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
|
||||||
}
|
}
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,24 @@
|
||||||
content_encoding: ""
|
content_encoding: ""
|
||||||
artifact_path: "abc.txt"
|
artifact_path: "abc.txt"
|
||||||
artifact_name: "artifact-download"
|
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
|
status: 1
|
||||||
created_unix: 1712338649
|
created_unix: 1712338649
|
||||||
updated_unix: 1712338649
|
updated_unix: 1712338649
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,7 @@ func prepareMigrationTasks() []*migration {
|
||||||
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
|
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(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
|
||||||
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
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
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
return perm.CanRead(unitType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PermissionNoAccess() Permission {
|
||||||
|
return Permission{AccessMode: perm_model.AccessModeNone}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,26 @@ package fileicon
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"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"
|
svgName := "octicon-file"
|
||||||
switch {
|
switch {
|
||||||
case entry.IsLink():
|
case entry.EntryMode.IsLink():
|
||||||
svgName = "octicon-file-symlink-file"
|
svgName = "octicon-file-symlink-file"
|
||||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
if entry.SymlinkToMode.IsDir() {
|
||||||
svgName = "octicon-file-directory-symlink"
|
svgName = "octicon-file-directory-symlink"
|
||||||
}
|
}
|
||||||
case entry.IsDir():
|
case entry.EntryMode.IsDir():
|
||||||
svgName = "octicon-file-directory-fill"
|
svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
|
||||||
case entry.IsSubModule():
|
case entry.EntryMode.IsSubModule():
|
||||||
svgName = "octicon-file-submodule"
|
svgName = "octicon-file-submodule"
|
||||||
}
|
}
|
||||||
return svg.RenderHTML(svgName)
|
return svgName
|
||||||
|
}
|
||||||
|
|
||||||
|
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
|
||||||
|
return svg.RenderHTML(BasicEntryIconName(entry))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/options"
|
"code.gitea.io/gitea/modules/options"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type materialIconRulesData struct {
|
type materialIconRulesData struct {
|
||||||
|
|
@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
|
||||||
}
|
}
|
||||||
svgID := "svg-mfi-" + name
|
svgID := "svg-mfi-" + name
|
||||||
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
||||||
|
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||||
|
if p == nil {
|
||||||
|
return svgHTML
|
||||||
|
}
|
||||||
if p.IconSVGs[svgID] == "" {
|
if p.IconSVGs[svgID] == "" {
|
||||||
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
p.IconSVGs[svgID] = svgHTML
|
||||||
}
|
}
|
||||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if m.rules == nil {
|
||||||
return BasicThemeIcon(entry)
|
return BasicEntryIconHTML(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.IsLink() {
|
if entry.EntryMode.IsLink() {
|
||||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
if entry.SymlinkToMode.IsDir() {
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
// 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("material-folder-symlink", 16, "octicon-file-directory-symlink")
|
||||||
}
|
}
|
||||||
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
|
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
|
||||||
}
|
}
|
||||||
|
|
||||||
name := m.findIconNameByGit(entry)
|
name := m.FindIconName(entry)
|
||||||
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
|
iconSVG := m.svgs[name]
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
if iconSVG == "" {
|
||||||
if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" {
|
name = "file"
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
if entry.EntryMode.IsDir() {
|
||||||
extraClass := "octicon-file"
|
name = util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||||
switch {
|
}
|
||||||
case entry.IsDir():
|
iconSVG = m.svgs[name]
|
||||||
extraClass = "octicon-file-directory-fill"
|
if iconSVG == "" {
|
||||||
case entry.IsSubModule():
|
setting.PanicInDevOrTesting("missing file icon for %s", name)
|
||||||
extraClass = "octicon-file-submodule"
|
|
||||||
}
|
}
|
||||||
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 {
|
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
||||||
|
|
@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
|
||||||
fileNameLower := strings.ToLower(path.Base(name))
|
if entry.EntryMode.IsSubModule() {
|
||||||
if isDir {
|
return "folder-git"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNameLower := strings.ToLower(path.Base(entry.FullName))
|
||||||
|
if entry.EntryMode.IsDir() {
|
||||||
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return "folder"
|
return util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, ok := m.rules.FileNames[fileNameLower]; ok {
|
if s, ok := m.rules.FileNames[fileNameLower]; ok {
|
||||||
|
|
@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
||||||
|
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
|
|
||||||
if entry.IsSubModule() {
|
|
||||||
return "folder-git"
|
|
||||||
}
|
|
||||||
return m.FindIconName(entry.Name(), entry.IsDir())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/fileicon"
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
|
||||||
func TestFindIconName(t *testing.T) {
|
func TestFindIconName(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
p := fileicon.DefaultMaterialIconProvider()
|
p := fileicon.DefaultMaterialIconProvider()
|
||||||
assert.Equal(t, "php", p.FindIconName("foo.php", false))
|
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
|
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "javascript", p.FindIconName("foo.js", false))
|
assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false))
|
assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
||||||
return template.HTML(sb.String())
|
return template.HTML(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
|
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||||
|
|
||||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
|
||||||
if setting.UI.FileIconTheme == "material" {
|
if setting.UI.FileIconTheme == "material" {
|
||||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
|
||||||
}
|
}
|
||||||
return BasicThemeIcon(entry)
|
return BasicEntryIconHTML(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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,31 @@ func (e EntryMode) String() string {
|
||||||
return strconv.FormatInt(int64(e), 8)
|
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) {
|
func ParseEntryMode(mode string) (EntryMode, error) {
|
||||||
switch mode {
|
switch mode {
|
||||||
case "000000":
|
case "000000":
|
||||||
|
|
|
||||||
|
|
@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
|
||||||
|
|
||||||
// IsSubModule if the entry is a sub module
|
// IsSubModule if the entry is a sub module
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
return te.entryMode == EntryModeCommit
|
return te.entryMode.IsSubModule()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
// IsDir if the entry is a sub dir
|
||||||
func (te *TreeEntry) IsDir() bool {
|
func (te *TreeEntry) IsDir() bool {
|
||||||
return te.entryMode == EntryModeTree
|
return te.entryMode.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
// IsLink if the entry is a symlink
|
||||||
func (te *TreeEntry) IsLink() bool {
|
func (te *TreeEntry) IsLink() bool {
|
||||||
return te.entryMode == EntryModeSymlink
|
return te.entryMode.IsLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
// IsRegular if the entry is a regular file
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
return te.entryMode == EntryModeBlob
|
return te.entryMode.IsRegular()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
return te.entryMode == EntryModeExec
|
return te.entryMode.IsExecutable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
|
|
|
||||||
|
|
@ -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"
|
CSRFCookieName = "_csrf"
|
||||||
CSRFCookieHTTPOnly = true
|
CSRFCookieHTTPOnly = true
|
||||||
RecordUserSignupMetadata = false
|
RecordUserSignupMetadata = false
|
||||||
|
TwoFactorAuthEnforced = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
|
// 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)
|
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
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")
|
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||||
if InstallLock && InternalToken == "" {
|
if InstallLock && InternalToken == "" {
|
||||||
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
|
// 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_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_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_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_userpass = Sign In
|
||||||
login_openid = OpenID
|
login_openid = OpenID
|
||||||
oauth_signup_tab = Register New Account
|
oauth_signup_tab = Register New Account
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,10 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
log.Error("Error getting artifacts: %v", err)
|
log.Error("Error getting artifacts: %v", err)
|
||||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
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{
|
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
|
||||||
RunID: runID,
|
RunID: runID,
|
||||||
ArtifactName: itemPath,
|
ArtifactName: itemPath,
|
||||||
|
Status: int(actions.ArtifactStatusUploadConfirmed),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error getting artifacts: %v", err)
|
log.Error("Error getting artifacts: %v", err)
|
||||||
|
|
@ -473,6 +477,11 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
|
||||||
ctx.HTTPError(http.StatusBadRequest)
|
ctx.HTTPError(http.StatusBadRequest)
|
||||||
return
|
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)
|
fd, err := ar.fs.Open(artifact.StoragePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -448,17 +448,15 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
log.Error("Error getting artifacts: %v", err)
|
log.Error("Error getting artifacts: %v", err)
|
||||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(artifacts) == 0 {
|
|
||||||
log.Debug("[artifact] handleListArtifacts, no artifacts")
|
|
||||||
ctx.HTTPError(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
list := []*ListArtifactsResponse_MonolithArtifact{}
|
list := []*ListArtifactsResponse_MonolithArtifact{}
|
||||||
|
|
||||||
|
|
@ -510,6 +508,11 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
|
||||||
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
|
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
|
||||||
return
|
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{}
|
respData := GetSignedArtifactURLResponse{}
|
||||||
|
|
||||||
|
|
@ -538,6 +541,11 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
|
||||||
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
|
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
|
||||||
return
|
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)
|
file, _ := r.fs.Open(artifact.StoragePath)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
gocontext "context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -211,11 +212,20 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
|
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
|
||||||
} else {
|
} else {
|
||||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
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() {
|
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) {
|
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||||
return func(ctx *context.APIContext) {
|
return func(ctx *context.APIContext) {
|
||||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
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/auth/source/sspi"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
|
||||||
"xorm.io/xorm/convert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -149,7 +147,6 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
||||||
RestrictedFilter: form.RestrictedFilter,
|
RestrictedFilter: form.RestrictedFilter,
|
||||||
AllowDeactivateAll: form.AllowDeactivateAll,
|
AllowDeactivateAll: form.AllowDeactivateAll,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +160,6 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
||||||
SkipVerify: form.SkipVerify,
|
SkipVerify: form.SkipVerify,
|
||||||
HeloHostname: form.HeloHostname,
|
HeloHostname: form.HeloHostname,
|
||||||
DisableHelo: form.DisableHelo,
|
DisableHelo: form.DisableHelo,
|
||||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,7 +194,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
RequiredClaimName: form.Oauth2RequiredClaimName,
|
RequiredClaimName: form.Oauth2RequiredClaimName,
|
||||||
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
||||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
||||||
GroupClaimName: form.Oauth2GroupClaimName,
|
GroupClaimName: form.Oauth2GroupClaimName,
|
||||||
RestrictedGroup: form.Oauth2RestrictedGroup,
|
RestrictedGroup: form.Oauth2RestrictedGroup,
|
||||||
AdminGroup: form.Oauth2AdminGroup,
|
AdminGroup: form.Oauth2AdminGroup,
|
||||||
|
|
@ -252,7 +247,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||||
|
|
||||||
hasTLS := false
|
hasTLS := false
|
||||||
var config convert.Conversion
|
var config auth.Config
|
||||||
switch auth.Type(form.Type) {
|
switch auth.Type(form.Type) {
|
||||||
case auth.LDAP, auth.DLDAP:
|
case auth.LDAP, auth.DLDAP:
|
||||||
config = parseLDAPConfig(form)
|
config = parseLDAPConfig(form)
|
||||||
|
|
@ -262,9 +257,8 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||||
hasTLS = true
|
hasTLS = true
|
||||||
case auth.PAM:
|
case auth.PAM:
|
||||||
config = &pam_service.Source{
|
config = &pam_service.Source{
|
||||||
ServiceName: form.PAMServiceName,
|
ServiceName: form.PAMServiceName,
|
||||||
EmailDomain: form.PAMEmailDomain,
|
EmailDomain: form.PAMEmailDomain,
|
||||||
SkipLocalTwoFA: form.SkipLocalTwoFA,
|
|
||||||
}
|
}
|
||||||
case auth.OAuth2:
|
case auth.OAuth2:
|
||||||
config = parseOAuth2Config(form)
|
config = parseOAuth2Config(form)
|
||||||
|
|
@ -302,11 +296,12 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := auth.CreateSource(ctx, &auth.Source{
|
if err := auth.CreateSource(ctx, &auth.Source{
|
||||||
Type: auth.Type(form.Type),
|
Type: auth.Type(form.Type),
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
IsActive: form.IsActive,
|
IsActive: form.IsActive,
|
||||||
IsSyncEnabled: form.IsSyncEnabled,
|
IsSyncEnabled: form.IsSyncEnabled,
|
||||||
Cfg: config,
|
TwoFactorPolicy: form.TwoFactorPolicy,
|
||||||
|
Cfg: config,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if auth.IsErrSourceAlreadyExist(err) {
|
if auth.IsErrSourceAlreadyExist(err) {
|
||||||
ctx.Data["Err_Name"] = true
|
ctx.Data["Err_Name"] = true
|
||||||
|
|
@ -384,7 +379,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var config convert.Conversion
|
var config auth.Config
|
||||||
switch auth.Type(form.Type) {
|
switch auth.Type(form.Type) {
|
||||||
case auth.LDAP, auth.DLDAP:
|
case auth.LDAP, auth.DLDAP:
|
||||||
config = parseLDAPConfig(form)
|
config = parseLDAPConfig(form)
|
||||||
|
|
@ -421,6 +416,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||||
source.IsActive = form.IsActive
|
source.IsActive = form.IsActive
|
||||||
source.IsSyncEnabled = form.IsSyncEnabled
|
source.IsSyncEnabled = form.IsSyncEnabled
|
||||||
source.Cfg = config
|
source.Cfg = config
|
||||||
|
source.TwoFactorPolicy = form.TwoFactorPolicy
|
||||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||||
if auth.IsErrSourceAlreadyExist(err) {
|
if auth.IsErrSourceAlreadyExist(err) {
|
||||||
ctx.Data["Err_Name"] = true
|
ctx.Data["Err_Name"] = true
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
|
@ -87,6 +88,7 @@ func TwoFactorPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||||
handleSignIn(ctx, u, remember)
|
handleSignIn(ctx, u, remember)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,10 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
isSucceed = true
|
isSucceed = true
|
||||||
|
|
||||||
|
|
@ -87,9 +91,9 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||||
|
|
||||||
if err := updateSession(ctx, nil, map[string]any{
|
if err := updateSession(ctx, nil, map[string]any{
|
||||||
// Set session IDs
|
session.KeyUID: u.ID,
|
||||||
"uid": u.ID,
|
session.KeyUname: u.Name,
|
||||||
"uname": u.Name,
|
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, fmt.Errorf("unable to updateSession: %w", err)
|
return false, fmt.Errorf("unable to updateSession: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -239,9 +243,8 @@ func SignInPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now handle 2FA:
|
// Now handle 2FA:
|
||||||
|
|
||||||
// First of all if the source can skip local two fa we're done
|
// 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)
|
handleSignIn(ctx, u, form.Remember)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +265,7 @@ func SignInPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasTOTPtwofa && !hasWebAuthnTwofa {
|
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)
|
handleSignIn(ctx, u, form.Remember)
|
||||||
return
|
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)
|
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{
|
if err := updateSession(ctx, []string{
|
||||||
// Delete the openid, 2fa and linkaccount data
|
// Delete the openid, 2fa and link_account data
|
||||||
"openid_verified_uri",
|
"openid_verified_uri",
|
||||||
"openid_signin_remember",
|
"openid_signin_remember",
|
||||||
"openid_determined_email",
|
"openid_determined_email",
|
||||||
|
|
@ -321,8 +330,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||||
"twofaRemember",
|
"twofaRemember",
|
||||||
"linkAccount",
|
"linkAccount",
|
||||||
}, map[string]any{
|
}, map[string]any{
|
||||||
"uid": u.ID,
|
session.KeyUID: u.ID,
|
||||||
"uname": u.Name,
|
session.KeyUname: u.Name,
|
||||||
|
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("RegenerateSession", err)
|
ctx.ServerError("RegenerateSession", err)
|
||||||
return setting.AppSubURL + "/"
|
return setting.AppSubURL + "/"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"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)
|
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
|
||||||
|
|
||||||
needs2FA := false
|
needs2FA := false
|
||||||
if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
|
if !source.TwoFactorShouldSkip() {
|
||||||
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
|
|
@ -352,10 +353,16 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||||
ctx.ServerError("UpdateUser", err)
|
ctx.ServerError("UpdateUser", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("UpdateUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := updateSession(ctx, nil, map[string]any{
|
if err := updateSession(ctx, nil, map[string]any{
|
||||||
"uid": u.ID,
|
session.KeyUID: u.ID,
|
||||||
"uname": u.Name,
|
session.KeyUname: u.Name,
|
||||||
|
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("updateSession", err)
|
ctx.ServerError("updateSession", err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
@ -369,7 +370,11 @@ func Diff(ctx *context.Context) {
|
||||||
return
|
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)
|
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
csv_module "code.gitea.io/gitea/modules/csv"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
@ -639,7 +640,11 @@ func PrepareCompareDiff(
|
||||||
return false
|
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)
|
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/emoji"
|
"code.gitea.io/gitea/modules/emoji"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
issue_template "code.gitea.io/gitea/modules/issue/template"
|
issue_template "code.gitea.io/gitea/modules/issue/template"
|
||||||
|
|
@ -823,7 +824,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
||||||
if reviewState != nil {
|
if reviewState != nil {
|
||||||
filesViewedState = reviewState.UpdatedFiles
|
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
|
ctx.Data["Diff"] = diff
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ type WebDiffFileItem struct {
|
||||||
EntryMode string
|
EntryMode string
|
||||||
IsViewed bool
|
IsViewed bool
|
||||||
Children []*WebDiffFileItem
|
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
|
// 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
|
// 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
|
// 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}
|
dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
|
||||||
addItem := func(item *WebDiffFileItem) {
|
addItem := func(item *WebDiffFileItem) {
|
||||||
var parentPath string
|
var parentPath string
|
||||||
|
|
@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
|
||||||
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
|
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
|
||||||
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
|
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
|
||||||
item.NameHash = git.HashFilePathForWebUI(item.FullName)
|
item.NameHash = git.HashFilePathForWebUI(item.FullName)
|
||||||
|
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
|
||||||
|
|
||||||
switch file.HeadMode {
|
switch file.HeadMode {
|
||||||
case git.EntryModeTree:
|
case git.EntryModeTree:
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
pull_model "code.gitea.io/gitea/models/pull"
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
|
|
||||||
|
|
@ -14,7 +16,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTransformDiffTreeForWeb(t *testing.T) {
|
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",
|
Status: "changed",
|
||||||
HeadPath: "dir-a/dir-a-x/file-deep",
|
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,
|
"dir-a/dir-a-x/file-deep": pull_model.Viewed,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockIconForFile := func(id string) template.HTML {
|
||||||
|
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||||
|
}
|
||||||
assert.Equal(t, WebDiffFileTree{
|
assert.Equal(t, WebDiffFileTree{
|
||||||
TreeRoot: WebDiffFileItem{
|
TreeRoot: WebDiffFileItem{
|
||||||
Children: []*WebDiffFileItem{
|
Children: []*WebDiffFileItem{
|
||||||
|
|
@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||||
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
|
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
|
||||||
DiffStatus: "changed",
|
DiffStatus: "changed",
|
||||||
IsViewed: true,
|
IsViewed: true,
|
||||||
|
FileIcon: mockIconForFile(`svg-mfi-file`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||||
FullName: "file1",
|
FullName: "file1",
|
||||||
NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
|
NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
|
||||||
DiffStatus: "added",
|
DiffStatus: "added",
|
||||||
|
FileIcon: mockIconForFile(`svg-mfi-file`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
|
||||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||||
fileIcons := map[string]template.HTML{}
|
fileIcons := map[string]template.HTML{}
|
||||||
for _, f := range files {
|
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["FileIcons"] = fileIcons
|
||||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
@ -163,6 +164,7 @@ func EnrollTwoFactor(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["Title"] = ctx.Tr("settings")
|
ctx.Data["Title"] = ctx.Tr("settings")
|
||||||
ctx.Data["PageIsSettingsSecurity"] = true
|
ctx.Data["PageIsSettingsSecurity"] = true
|
||||||
|
ctx.Data["ShowTwoFactorRequiredMessage"] = false
|
||||||
|
|
||||||
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||||
if t != nil {
|
if t != nil {
|
||||||
|
|
@ -194,6 +196,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
|
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
|
||||||
ctx.Data["Title"] = ctx.Tr("settings")
|
ctx.Data["Title"] = ctx.Tr("settings")
|
||||||
ctx.Data["PageIsSettingsSecurity"] = true
|
ctx.Data["PageIsSettingsSecurity"] = true
|
||||||
|
ctx.Data["ShowTwoFactorRequiredMessage"] = false
|
||||||
|
|
||||||
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||||
if t != nil {
|
if t != nil {
|
||||||
|
|
@ -246,6 +249,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||||
return
|
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
|
// 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 we can detect the unique constraint failure below we can move this to after the NewTwoFactor
|
||||||
if err := ctx.Session.Delete("twofaSecret"); err != nil {
|
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)
|
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.
|
// 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
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
wa "code.gitea.io/gitea/modules/auth/webauthn"
|
wa "code.gitea.io/gitea/modules/auth/webauthn"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
@ -120,7 +121,7 @@ func WebauthnRegisterPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = ctx.Session.Delete("webauthnName")
|
_ = ctx.Session.Delete("webauthnName")
|
||||||
|
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||||
ctx.JSON(http.StatusCreated, cred)
|
ctx.JSON(http.StatusCreated, cred)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,14 +142,14 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
if !source.TwoFactorShouldSkip() {
|
||||||
// Check if the user has webAuthn registration
|
// Check if the user has WebAuthn registration
|
||||||
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if hasWebAuthn {
|
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 {
|
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)
|
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
|
// SynchronizableSource represents a source that can synchronize users
|
||||||
type SynchronizableSource interface {
|
type SynchronizableSource interface {
|
||||||
Sync(ctx context.Context, updateExisting bool) error
|
Sync(ctx context.Context, updateExisting bool) error
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Source is a password authentication service
|
// Source is a password authentication service
|
||||||
type Source struct{}
|
type Source struct {
|
||||||
|
auth.ConfigBase `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
// FromDB fills up an OAuth2Config from serialized format.
|
// FromDB fills up an OAuth2Config from serialized format.
|
||||||
func (source *Source) FromDB(bs []byte) error {
|
func (source *Source) FromDB(bs []byte) error {
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,11 @@ import (
|
||||||
type sourceInterface interface {
|
type sourceInterface interface {
|
||||||
auth.PasswordAuthenticator
|
auth.PasswordAuthenticator
|
||||||
auth.SynchronizableSource
|
auth.SynchronizableSource
|
||||||
auth.LocalTwoFASkipper
|
|
||||||
auth_model.SSHKeyProvider
|
auth_model.SSHKeyProvider
|
||||||
auth_model.Config
|
auth_model.Config
|
||||||
auth_model.SkipVerifiable
|
auth_model.SkipVerifiable
|
||||||
auth_model.HasTLSer
|
auth_model.HasTLSer
|
||||||
auth_model.UseTLSer
|
auth_model.UseTLSer
|
||||||
auth_model.SourceSettable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ (sourceInterface) = &ldap.Source{}
|
var _ (sourceInterface) = &ldap.Source{}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import (
|
||||||
|
|
||||||
// Source Basic LDAP authentication service
|
// Source Basic LDAP authentication service
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
auth.ConfigBase `json:"-"`
|
||||||
|
|
||||||
Name string // canonical name (ie. corporate.ad)
|
Name string // canonical name (ie. corporate.ad)
|
||||||
Host string // LDAP host
|
Host string // LDAP host
|
||||||
Port int // port number
|
Port int // port number
|
||||||
|
|
@ -54,9 +56,6 @@ type Source struct {
|
||||||
GroupTeamMap string // Map LDAP groups to teams
|
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
|
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
|
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.
|
// FromDB fills up a LDAPConfig from serialized format.
|
||||||
|
|
@ -109,11 +108,6 @@ func (source *Source) ProvidesSSHKeys() bool {
|
||||||
return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthSource sets the related AuthSource
|
|
||||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
|
||||||
source.authSource = authSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
auth.RegisterTypeConfig(auth.LDAP, &Source{})
|
auth.RegisterTypeConfig(auth.LDAP, &Source{})
|
||||||
auth.RegisterTypeConfig(auth.DLDAP, &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 {
|
if user != nil {
|
||||||
loginName = user.LoginName
|
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 {
|
if sr == nil {
|
||||||
// User not in LDAP, do nothing
|
// User not in LDAP, do nothing
|
||||||
return nil, user_model.ErrUserNotExist{Name: loginName}
|
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 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 {
|
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
@ -84,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
Name: sr.Username,
|
Name: sr.Username,
|
||||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||||
Email: sr.Mail,
|
Email: sr.Mail,
|
||||||
LoginType: source.authSource.Type,
|
LoginType: source.AuthSource.Type,
|
||||||
LoginSource: source.authSource.ID,
|
LoginSource: source.AuthSource.ID,
|
||||||
LoginName: userName,
|
LoginName: userName,
|
||||||
IsAdmin: sr.IsAdmin,
|
IsAdmin: sr.IsAdmin,
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
return user, err
|
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 {
|
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
@ -123,8 +123,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
|
|
||||||
return user, nil
|
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
|
// Sync causes this ldap source to synchronize its users with the db
|
||||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
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) != ""
|
isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
||||||
var sshKeysNeedUpdate bool
|
var sshKeysNeedUpdate bool
|
||||||
|
|
||||||
// Find all users with this login type - FIXME: Should this be an iterator?
|
// 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 {
|
if err != nil {
|
||||||
log.Error("SyncExternalUsers: %v", err)
|
log.Error("SyncExternalUsers: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Warn("SyncExternalUsers: Cancelled 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)
|
return db.ErrCancelledf("Before update of %s", source.AuthSource.Name)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
|
|
||||||
sr, err := source.SearchEntries()
|
sr, err := source.SearchEntries()
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
for _, su := range sr {
|
for _, su := range sr {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
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
|
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
||||||
if sshKeysNeedUpdate {
|
if sshKeysNeedUpdate {
|
||||||
err = asymkey_service.RewriteAllPublicKeys(ctx)
|
err = asymkey_service.RewriteAllPublicKeys(ctx)
|
||||||
|
|
@ -82,7 +82,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
log.Error("RewriteAllPublicKeys: %v", err)
|
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:
|
default:
|
||||||
}
|
}
|
||||||
if su.Username == "" && su.Mail == "" {
|
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)
|
fullName := composeFullName(su.Name, su.Surname, su.Username)
|
||||||
// If no existing user found, create one
|
// If no existing user found, create one
|
||||||
if usr == nil {
|
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{
|
usr = &user_model.User{
|
||||||
LowerName: su.LowerName,
|
LowerName: su.LowerName,
|
||||||
Name: su.Username,
|
Name: su.Username,
|
||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
LoginType: source.authSource.Type,
|
LoginType: source.AuthSource.Type,
|
||||||
LoginSource: source.authSource.ID,
|
LoginSource: source.AuthSource.ID,
|
||||||
LoginName: su.Username,
|
LoginName: su.Username,
|
||||||
Email: su.Mail,
|
Email: su.Mail,
|
||||||
IsAdmin: su.IsAdmin,
|
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)
|
err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
|
||||||
if err != nil {
|
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 {
|
if err == nil && isAttributeSSHPublicKeySet {
|
||||||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.authSource.Name, usr.Name)
|
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) {
|
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
|
||||||
sshKeysNeedUpdate = true
|
sshKeysNeedUpdate = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
}
|
}
|
||||||
} else if updateExisting {
|
} else if updateExisting {
|
||||||
// Synchronize SSH Public Key if that attribute is set
|
// 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
|
sshKeysNeedUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
!strings.EqualFold(usr.Email, su.Mail) ||
|
!strings.EqualFold(usr.Email, su.Mail) ||
|
||||||
usr.FullName != fullName ||
|
usr.FullName != fullName ||
|
||||||
!usr.IsActive {
|
!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{
|
opts := &user_service.UpdateOptions{
|
||||||
FullName: optional.Some(fullName),
|
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 {
|
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 {
|
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 {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Warn("SyncExternalUsers: Cancelled 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)
|
return db.ErrCancelledf("During update of %s before delete users", source.AuthSource.Name)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,13 +214,13 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||||
continue
|
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{
|
opts := &user_service.UpdateOptions{
|
||||||
IsActive: optional.Some(false),
|
IsActive: optional.Some(false),
|
||||||
}
|
}
|
||||||
if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
|
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 {
|
type sourceInterface interface {
|
||||||
auth_model.Config
|
auth_model.Config
|
||||||
auth_model.SourceSettable
|
|
||||||
auth_model.RegisterableSource
|
auth_model.RegisterableSource
|
||||||
auth.PasswordAuthenticator
|
auth.PasswordAuthenticator
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import (
|
||||||
|
|
||||||
// Source holds configuration for the OAuth2 login source.
|
// Source holds configuration for the OAuth2 login source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
auth.ConfigBase `json:"-"`
|
||||||
|
|
||||||
Provider string
|
Provider string
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
|
|
@ -25,10 +27,6 @@ type Source struct {
|
||||||
GroupTeamMap string
|
GroupTeamMap string
|
||||||
GroupTeamMapRemoval bool
|
GroupTeamMapRemoval bool
|
||||||
RestrictedGroup string
|
RestrictedGroup string
|
||||||
SkipLocalTwoFA bool `json:",omitempty"`
|
|
||||||
|
|
||||||
// reference to the authSource
|
|
||||||
authSource *auth.Source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDB fills up an OAuth2Config from serialized format.
|
// FromDB fills up an OAuth2Config from serialized format.
|
||||||
|
|
@ -41,11 +39,6 @@ func (source *Source) ToDB() ([]byte, error) {
|
||||||
return json.Marshal(source)
|
return json.Marshal(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthSource sets the related AuthSource
|
|
||||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
|
||||||
source.authSource = authSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
|
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
// Callout redirects request/response pair to authenticate against the provider
|
// Callout redirects request/response pair to authenticate against the provider
|
||||||
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
|
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
|
||||||
// not sure if goth is thread safe (?) when using multiple providers
|
// 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
|
// 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
|
// 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
|
// 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) {
|
func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
|
||||||
// not sure if goth is thread safe (?) when using multiple providers
|
// 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()
|
gothRWMutex.RLock()
|
||||||
defer gothRWMutex.RUnlock()
|
defer gothRWMutex.RUnlock()
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ import (
|
||||||
|
|
||||||
// RegisterSource causes an OAuth2 configuration to be registered
|
// RegisterSource causes an OAuth2 configuration to be registered
|
||||||
func (source *Source) RegisterSource() error {
|
func (source *Source) RegisterSource() error {
|
||||||
err := RegisterProviderWithGothic(source.authSource.Name, source)
|
err := RegisterProviderWithGothic(source.AuthSource.Name, source)
|
||||||
return wrapOpenIDConnectInitializeError(err, source.authSource.Name, source)
|
return wrapOpenIDConnectInitializeError(err, source.AuthSource.Name, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterSource causes an OAuth2 configuration to be unregistered
|
// UnregisterSource causes an OAuth2 configuration to be unregistered
|
||||||
func (source *Source) UnregisterSource() error {
|
func (source *Source) UnregisterSource() error {
|
||||||
RemoveProviderFromGothic(source.authSource.Name)
|
RemoveProviderFromGothic(source.AuthSource.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,27 +18,27 @@ import (
|
||||||
|
|
||||||
// Sync causes this OAuth2 source to synchronize its users with the db.
|
// Sync causes this OAuth2 source to synchronize its users with the db.
|
||||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := createProvider(source.authSource.Name, source)
|
provider, err := createProvider(source.AuthSource.Name, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !provider.RefreshTokenAvailable() {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := user_model.FindExternalUserOptions{
|
opts := user_model.FindExternalUserOptions{
|
||||||
HasRefreshToken: true,
|
HasRefreshToken: true,
|
||||||
Expired: 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 {
|
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
|
// recognizes them as a valid user, they will be able to login
|
||||||
// via their provider and reactivate their account.
|
// via their provider and reactivate their account.
|
||||||
if shouldDisable {
|
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 {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if hasUser {
|
if hasUser {
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,21 @@ func TestSource(t *testing.T) {
|
||||||
|
|
||||||
source := &Source{
|
source := &Source{
|
||||||
Provider: "fake",
|
Provider: "fake",
|
||||||
authSource: &auth.Source{
|
ConfigBase: auth.ConfigBase{
|
||||||
ID: 12,
|
AuthSource: &auth.Source{
|
||||||
Type: auth.OAuth2,
|
ID: 12,
|
||||||
Name: "fake",
|
Type: auth.OAuth2,
|
||||||
IsActive: true,
|
Name: "fake",
|
||||||
IsSyncEnabled: true,
|
IsActive: true,
|
||||||
|
IsSyncEnabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &user_model.User{
|
user := &user_model.User{
|
||||||
LoginName: "external",
|
LoginName: "external",
|
||||||
LoginType: auth.OAuth2,
|
LoginType: auth.OAuth2,
|
||||||
LoginSource: source.authSource.ID,
|
LoginSource: source.AuthSource.ID,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Email: "external@example.com",
|
Email: "external@example.com",
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +49,7 @@ func TestSource(t *testing.T) {
|
||||||
err = user_model.LinkExternalToUser(t.Context(), user, e)
|
err = user_model.LinkExternalToUser(t.Context(), user, e)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
provider, err := createProvider(source.authSource.Name, source)
|
provider, err := createProvider(source.AuthSource.Name, source)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
t.Run("refresh", func(t *testing.T) {
|
t.Run("refresh", func(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import (
|
||||||
type sourceInterface interface {
|
type sourceInterface interface {
|
||||||
auth.PasswordAuthenticator
|
auth.PasswordAuthenticator
|
||||||
auth_model.Config
|
auth_model.Config
|
||||||
auth_model.SourceSettable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ (sourceInterface) = &pam.Source{}
|
var _ (sourceInterface) = &pam.Source{}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,10 @@ import (
|
||||||
|
|
||||||
// Source holds configuration for the PAM login source.
|
// Source holds configuration for the PAM login source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ServiceName string // pam service (e.g. system-auth)
|
auth.ConfigBase `json:"-"`
|
||||||
EmailDomain string
|
|
||||||
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
|
|
||||||
|
|
||||||
// reference to the authSource
|
ServiceName string // pam service (e.g. system-auth)
|
||||||
authSource *auth.Source
|
EmailDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDB fills up a PAMConfig from serialized format.
|
// FromDB fills up a PAMConfig from serialized format.
|
||||||
|
|
@ -35,11 +33,6 @@ func (source *Source) ToDB() ([]byte, error) {
|
||||||
return json.Marshal(source)
|
return json.Marshal(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthSource sets the related AuthSource
|
|
||||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
|
||||||
source.authSource = authSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
auth.RegisterTypeConfig(auth.PAM, &Source{})
|
auth.RegisterTypeConfig(auth.PAM, &Source{})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
Email: email,
|
Email: email,
|
||||||
Passwd: password,
|
Passwd: password,
|
||||||
LoginType: auth.PAM,
|
LoginType: auth.PAM,
|
||||||
LoginSource: source.authSource.ID,
|
LoginSource: source.AuthSource.ID,
|
||||||
LoginName: userName, // This is what the user typed in
|
LoginName: userName, // This is what the user typed in
|
||||||
}
|
}
|
||||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||||
|
|
@ -69,8 +69,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
|
|
||||||
return user, nil
|
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.SkipVerifiable
|
||||||
auth_model.HasTLSer
|
auth_model.HasTLSer
|
||||||
auth_model.UseTLSer
|
auth_model.UseTLSer
|
||||||
auth_model.SourceSettable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ (sourceInterface) = &smtp.Source{}
|
var _ (sourceInterface) = &smtp.Source{}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import (
|
||||||
|
|
||||||
// Source holds configuration for the SMTP login source.
|
// Source holds configuration for the SMTP login source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
auth.ConfigBase `json:"-"`
|
||||||
|
|
||||||
Auth string
|
Auth string
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
|
|
@ -25,10 +27,6 @@ type Source struct {
|
||||||
SkipVerify bool
|
SkipVerify bool
|
||||||
HeloHostname string
|
HeloHostname string
|
||||||
DisableHelo bool
|
DisableHelo bool
|
||||||
SkipLocalTwoFA bool `json:",omitempty"`
|
|
||||||
|
|
||||||
// reference to the authSource
|
|
||||||
authSource *auth.Source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDB fills up an SMTPConfig from serialized format.
|
// FromDB fills up an SMTPConfig from serialized format.
|
||||||
|
|
@ -56,11 +54,6 @@ func (source *Source) UseTLS() bool {
|
||||||
return source.ForceSMTPS || source.Port == 465
|
return source.ForceSMTPS || source.Port == 465
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthSource sets the related AuthSource
|
|
||||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
|
||||||
source.authSource = authSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
auth.RegisterTypeConfig(auth.SMTP, &Source{})
|
auth.RegisterTypeConfig(auth.SMTP, &Source{})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
Email: userName,
|
Email: userName,
|
||||||
Passwd: password,
|
Passwd: password,
|
||||||
LoginType: auth_model.SMTP,
|
LoginType: auth_model.SMTP,
|
||||||
LoginSource: source.authSource.ID,
|
LoginSource: source.AuthSource.ID,
|
||||||
LoginName: userName,
|
LoginName: userName,
|
||||||
}
|
}
|
||||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||||
|
|
@ -85,8 +85,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||||
|
|
||||||
return user, nil
|
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.
|
// Source holds configuration for SSPI single sign-on.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
auth.ConfigBase `json:"-"`
|
||||||
|
|
||||||
AutoCreateUsers bool
|
AutoCreateUsers bool
|
||||||
AutoActivateUsers bool
|
AutoActivateUsers bool
|
||||||
StripDomainNames bool
|
StripDomainNames bool
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,8 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
|
|
||||||
ctx.Data["SystemConfig"] = setting.Config()
|
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
|
// 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["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||||
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
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.
|
// HasError returns true if error occurs in form validation.
|
||||||
// Attention: this function changes ctx.Data and ctx.Flash
|
// 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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
if ctx.DoerNeedTwoFactorAuth() {
|
||||||
if err != nil {
|
ctx.Repo.Permission = access_model.PermissionNoAccess()
|
||||||
ctx.ServerError("GetUserRepoPermission", err)
|
} else {
|
||||||
return
|
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) {
|
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@ import (
|
||||||
|
|
||||||
// AuthenticationForm form for authentication
|
// AuthenticationForm form for authentication
|
||||||
type AuthenticationForm struct {
|
type AuthenticationForm struct {
|
||||||
ID int64
|
ID int64
|
||||||
Type int `binding:"Range(2,7)"`
|
Type int `binding:"Range(2,7)"`
|
||||||
Name string `binding:"Required;MaxSize(30)"`
|
Name string `binding:"Required;MaxSize(30)"`
|
||||||
|
TwoFactorPolicy string
|
||||||
|
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
BindDN string
|
BindDN string
|
||||||
|
|
@ -74,7 +76,6 @@ type AuthenticationForm struct {
|
||||||
Oauth2RestrictedGroup string
|
Oauth2RestrictedGroup string
|
||||||
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||||
Oauth2GroupTeamMapRemoval bool
|
Oauth2GroupTeamMapRemoval bool
|
||||||
SkipLocalTwoFA bool
|
|
||||||
SSPIAutoCreateUsers bool
|
SSPIAutoCreateUsers bool
|
||||||
SSPIAutoActivateUsers bool
|
SSPIAutoActivateUsers bool
|
||||||
SSPIStripDomainNames bool
|
SSPIStripDomainNames bool
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,6 @@ func IsCodeOwnerFile(f string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
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 {
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -100,19 +96,15 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if startCommitID == "" && endCommitID == "" {
|
// get the mergebase
|
||||||
// get the mergebase
|
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
||||||
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
startCommitID = mergeBase
|
|
||||||
endCommitID = pr.GetGitRefName()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
|
// 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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +130,23 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
|
||||||
return nil, err
|
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 {
|
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)
|
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
|
||||||
if err != nil {
|
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)
|
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 {
|
for _, t := range uniqTeams {
|
||||||
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
|
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -463,12 +463,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pr.IsWorkInProgress(ctx) {
|
if !pr.IsWorkInProgress(ctx) {
|
||||||
var reviewNotifiers []*issue_service.ReviewRequestNotifier
|
reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
||||||
if opts.IsForcePush {
|
|
||||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
|
||||||
} else {
|
|
||||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, opts.OldCommitID, opts.NewCommitID)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("PullRequestCodeOwnersReview: %v", err)
|
log.Error("PullRequestCodeOwnersReview: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,19 +165,11 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re
|
||||||
FullPath: path.Join(parentDir, entry.Name()),
|
FullPath: path.Join(parentDir, entry.Name()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.IsLink() {
|
entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
|
||||||
// TODO: symlink to a folder or a file, the icon differs
|
node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||||
target, err := entry.FollowLink()
|
if entryInfo.EntryMode.IsDir() {
|
||||||
if err == nil {
|
entryInfo.IsOpen = true
|
||||||
_ = target.IsDir()
|
node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.EntryMode == "commit" {
|
if node.EntryMode == "commit" {
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,18 @@ func TestGetTreeViewNodes(t *testing.T) {
|
||||||
mockIconForFolder := func(id string) template.HTML {
|
mockIconForFolder := func(id string) template.HTML {
|
||||||
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||||
}
|
}
|
||||||
|
mockOpenIconForFolder := func(id string) template.HTML {
|
||||||
|
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||||
|
}
|
||||||
treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
|
treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []*TreeViewNode{
|
assert.Equal(t, []*TreeViewNode{
|
||||||
{
|
{
|
||||||
EntryName: "docs",
|
EntryName: "docs",
|
||||||
EntryMode: "tree",
|
EntryMode: "tree",
|
||||||
FullPath: "docs",
|
FullPath: "docs",
|
||||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||||
|
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
|
||||||
},
|
},
|
||||||
}, treeNodes)
|
}, treeNodes)
|
||||||
|
|
||||||
|
|
@ -86,10 +90,11 @@ func TestGetTreeViewNodes(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []*TreeViewNode{
|
assert.Equal(t, []*TreeViewNode{
|
||||||
{
|
{
|
||||||
EntryName: "docs",
|
EntryName: "docs",
|
||||||
EntryMode: "tree",
|
EntryMode: "tree",
|
||||||
FullPath: "docs",
|
FullPath: "docs",
|
||||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||||
|
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
|
||||||
Children: []*TreeViewNode{
|
Children: []*TreeViewNode{
|
||||||
{
|
{
|
||||||
EntryName: "README.md",
|
EntryName: "README.md",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,13 @@
|
||||||
<label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label>
|
<label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label>
|
||||||
<input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required>
|
<input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required>
|
||||||
</div>
|
</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 -->
|
<!-- LDAP and DLDAP -->
|
||||||
{{if or .Source.IsLDAP .Source.IsDLDAP}}
|
{{if or .Source.IsLDAP .Source.IsDLDAP}}
|
||||||
|
|
@ -159,13 +166,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label for="allow_deactivate_all"><strong>{{ctx.Locale.Tr "admin.auths.allow_deactivate_all"}}</strong></label>
|
<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}}">
|
<input id="allowed_domains" name="allowed_domains" value="{{$cfg.AllowedDomains}}">
|
||||||
<p class="help">{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}</p>
|
<p class="help">{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}</p>
|
||||||
</div>
|
</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}}
|
{{end}}
|
||||||
|
|
||||||
<!-- PAM -->
|
<!-- PAM -->
|
||||||
|
|
@ -247,13 +240,6 @@
|
||||||
<label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label>
|
<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}}">
|
<input id="pam_email_domain" name="pam_email_domain" value="{{$cfg.EmailDomain}}">
|
||||||
</div>
|
</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}}
|
{{end}}
|
||||||
|
|
||||||
<!-- OAuth2 -->
|
<!-- OAuth2 -->
|
||||||
|
|
@ -288,13 +274,6 @@
|
||||||
<label for="open_id_connect_auto_discovery_url">{{ctx.Locale.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
|
<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}}">
|
<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{$cfg.OpenIDConnectAutoDiscoveryURL}}">
|
||||||
</div>
|
</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="oauth2_use_custom_url inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label><strong>{{ctx.Locale.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>
|
<label><strong>{{ctx.Locale.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,8 @@
|
||||||
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
|
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- 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 -}}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<div id="diff-container">
|
<div id="diff-container">
|
||||||
{{if $showFileTree}}
|
{{if $showFileTree}}
|
||||||
|
{{$.FileIconPoolHTML}}
|
||||||
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
|
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
|
||||||
<script>
|
<script>
|
||||||
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
|
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
{{template "repo/latest_commit" .}}
|
{{template "repo/latest_commit" .}}
|
||||||
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{$.FileIconPoolHTML}}
|
||||||
{{if .HasParentPath}}
|
{{if .HasParentPath}}
|
||||||
<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
|
<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
|
||||||
{{svg "octicon-file-directory-fill"}} ..
|
{{index $.FileIcons ".."}} ..
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$.FileIconPoolHTML}}
|
|
||||||
{{range $item := .Files}}
|
{{range $item := .Files}}
|
||||||
<div class="repo-file-item">
|
<div class="repo-file-item">
|
||||||
{{$entry := $item.Entry}}
|
{{$entry := $item.Entry}}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
|
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
|
||||||
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
|
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
<div class="status-page-error">
|
<div class="status-page-error">
|
||||||
<div class="status-page-error-title">404 Not Found</div>
|
<div class="status-page-error-title">404 Not Found</div>
|
||||||
<div class="tw-text-center">
|
<div class="tw-text-center">
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,26 @@ func TestActionsArtifactV4Delete(t *testing.T) {
|
||||||
var deleteResp actions.DeleteArtifactResponse
|
var deleteResp actions.DeleteArtifactResponse
|
||||||
protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
|
protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
|
||||||
assert.True(t, deleteResp.Ok)
|
assert.True(t, deleteResp.Ok)
|
||||||
|
|
||||||
|
// confirm artifact is no longer accessible by GetSignedArtifactURL
|
||||||
|
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
|
||||||
|
Name: "artifact-v4-download",
|
||||||
|
WorkflowRunBackendId: "792",
|
||||||
|
WorkflowJobRunBackendId: "193",
|
||||||
|
})).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
_ = MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
// confirm artifact is no longer enumerateable by ListArtifacts and returns length == 0 without error
|
||||||
|
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
|
||||||
|
NameFilter: wrapperspb.String("artifact-v4-download"),
|
||||||
|
WorkflowRunBackendId: "792",
|
||||||
|
WorkflowJobRunBackendId: "193",
|
||||||
|
})).AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp actions.ListArtifactsResponse
|
||||||
|
protojson.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.Empty(t, listResp.Artifacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActionsArtifactV4DeletePublicApi(t *testing.T) {
|
func TestActionsArtifactV4DeletePublicApi(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ func TestBasicAuthWithWebAuthn(t *testing.T) {
|
||||||
}
|
}
|
||||||
var userParsed userResponse
|
var userParsed userResponse
|
||||||
DecodeJSON(t, resp, &userParsed)
|
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
|
// user32 has webauthn enrolled, he can't request git protocol with basic auth
|
||||||
req = NewRequest(t, "GET", "/user2/repo1/info/refs")
|
req = NewRequest(t, "GET", "/user2/repo1/info/refs")
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,6 @@ function getIconForDiffStatus(pType: DiffStatus) {
|
||||||
};
|
};
|
||||||
return diffTypes[pType] ?? diffTypes[''];
|
return diffTypes[pType] ?? diffTypes[''];
|
||||||
}
|
}
|
||||||
|
|
||||||
function entryIcon(entry: DiffTreeEntry) {
|
|
||||||
if (entry.EntryMode === 'commit') {
|
|
||||||
return 'octicon-file-submodule';
|
|
||||||
}
|
|
||||||
return 'octicon-file';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -36,10 +29,8 @@ function entryIcon(entry: DiffTreeEntry) {
|
||||||
<div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
|
<div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
|
||||||
<!-- directory -->
|
<!-- directory -->
|
||||||
<SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
|
<SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
|
||||||
<SvgIcon
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
class="text primary"
|
<span class="tw-contents" v-html="collapsed ? store.folderIcon : store.folderOpenIcon"/>
|
||||||
:name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
|
|
||||||
/>
|
|
||||||
<span class="gt-ellipsis">{{ item.DisplayName }}</span>
|
<span class="gt-ellipsis">{{ item.DisplayName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -53,7 +44,8 @@ function entryIcon(entry: DiffTreeEntry) {
|
||||||
:title="item.DisplayName" :href="'#diff-' + item.NameHash"
|
:title="item.DisplayName" :href="'#diff-' + item.NameHash"
|
||||||
>
|
>
|
||||||
<!-- file -->
|
<!-- file -->
|
||||||
<SvgIcon :name="entryIcon(item)"/>
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span class="tw-contents" v-html="item.FileIcon"/>
|
||||||
<span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
|
<span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
:name="getIconForDiffStatus(item.DiffStatus).name"
|
:name="getIconForDiffStatus(item.DiffStatus).name"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ test('diff-tree', () => {
|
||||||
'IsViewed': false,
|
'IsViewed': false,
|
||||||
'NameHash': '....',
|
'NameHash': '....',
|
||||||
'DiffStatus': '',
|
'DiffStatus': '',
|
||||||
|
'FileIcon': '',
|
||||||
'Children': [
|
'Children': [
|
||||||
{
|
{
|
||||||
'FullName': 'dir1',
|
'FullName': 'dir1',
|
||||||
|
|
@ -17,6 +18,7 @@ test('diff-tree', () => {
|
||||||
'IsViewed': false,
|
'IsViewed': false,
|
||||||
'NameHash': '....',
|
'NameHash': '....',
|
||||||
'DiffStatus': '',
|
'DiffStatus': '',
|
||||||
|
'FileIcon': '',
|
||||||
'Children': [
|
'Children': [
|
||||||
{
|
{
|
||||||
'FullName': 'dir1/test.txt',
|
'FullName': 'dir1/test.txt',
|
||||||
|
|
@ -25,6 +27,7 @@ test('diff-tree', () => {
|
||||||
'NameHash': '....',
|
'NameHash': '....',
|
||||||
'EntryMode': '',
|
'EntryMode': '',
|
||||||
'IsViewed': false,
|
'IsViewed': false,
|
||||||
|
'FileIcon': '',
|
||||||
'Children': null,
|
'Children': null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -36,11 +39,12 @@ test('diff-tree', () => {
|
||||||
'DiffStatus': 'added',
|
'DiffStatus': 'added',
|
||||||
'EntryMode': '',
|
'EntryMode': '',
|
||||||
'IsViewed': false,
|
'IsViewed': false,
|
||||||
|
'FileIcon': '',
|
||||||
'Children': null,
|
'Children': null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
}, '', '');
|
||||||
diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
|
diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
|
||||||
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
|
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
|
||||||
expect(store.fullNameMap['dir1'].IsViewed).toBe(true);
|
expect(store.fullNameMap['dir1'].IsViewed).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export type DiffTreeEntry = {
|
||||||
EntryMode: string,
|
EntryMode: string,
|
||||||
IsViewed: boolean,
|
IsViewed: boolean,
|
||||||
Children: DiffTreeEntry[],
|
Children: DiffTreeEntry[],
|
||||||
|
FileIcon: string,
|
||||||
ParentEntry?: DiffTreeEntry,
|
ParentEntry?: DiffTreeEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +22,8 @@ type DiffFileTreeData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type DiffFileTree = {
|
type DiffFileTree = {
|
||||||
|
folderIcon: string;
|
||||||
|
folderOpenIcon: string;
|
||||||
diffFileTree: DiffFileTreeData;
|
diffFileTree: DiffFileTreeData;
|
||||||
fullNameMap?: Record<string, DiffTreeEntry>
|
fullNameMap?: Record<string, DiffTreeEntry>
|
||||||
fileTreeIsVisible: boolean;
|
fileTreeIsVisible: boolean;
|
||||||
|
|
@ -31,7 +33,7 @@ type DiffFileTree = {
|
||||||
let diffTreeStoreReactive: Reactive<DiffFileTree>;
|
let diffTreeStoreReactive: Reactive<DiffFileTree>;
|
||||||
export function diffTreeStore() {
|
export function diffTreeStore() {
|
||||||
if (!diffTreeStoreReactive) {
|
if (!diffTreeStoreReactive) {
|
||||||
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree);
|
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon);
|
||||||
}
|
}
|
||||||
return diffTreeStoreReactive;
|
return diffTreeStoreReactive;
|
||||||
}
|
}
|
||||||
|
|
@ -55,9 +57,11 @@ function fillFullNameMap(map: Record<string, DiffTreeEntry>, entry: DiffTreeEntr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reactiveDiffTreeStore(data: DiffFileTreeData): Reactive<DiffFileTree> {
|
export function reactiveDiffTreeStore(data: DiffFileTreeData, folderIcon: string, folderOpenIcon: string): Reactive<DiffFileTree> {
|
||||||
const store = reactive({
|
const store = reactive({
|
||||||
diffFileTree: data,
|
diffFileTree: data,
|
||||||
|
folderIcon,
|
||||||
|
folderOpenIcon,
|
||||||
fileTreeIsVisible: false,
|
fileTreeIsVisible: false,
|
||||||
selectedItem: '',
|
selectedItem: '',
|
||||||
fullNameMap: {},
|
fullNameMap: {},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue